Merge branch 'develop' into renovate/await-lock-3.x
49
CHANGELOG.md
|
|
@ -1,3 +1,52 @@
|
|||
Changes in [1.12.7](https://github.com/element-hq/element-web/releases/tag/v1.12.7) (2025-12-16)
|
||||
================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Replace legacy icons with compound ([#31424](https://github.com/element-hq/element-web/pull/31424)). Contributed by @t3chguy.
|
||||
* Update polls UX to match EX Mobile and improve accessibility ([#31245](https://github.com/element-hq/element-web/pull/31245)). Contributed by @langleyd.
|
||||
* Add option to enable read receipt and marker when user interact with UI ([#31353](https://github.com/element-hq/element-web/pull/31353)). Contributed by @florianduros.
|
||||
* Introduce a hook to auto dispose view models ([#31178](https://github.com/element-hq/element-web/pull/31178)). Contributed by @MidhunSureshR.
|
||||
* Update settings toggles to use consistent design across app. ([#30169](https://github.com/element-hq/element-web/pull/30169)). Contributed by @Half-Shot.
|
||||
* Add ability to the room view to hide widgets ([#31400](https://github.com/element-hq/element-web/pull/31400)). Contributed by @langleyd.
|
||||
* call: Pass the echo cancellation and noise suppression settings to EC ([#31317](https://github.com/element-hq/element-web/pull/31317)). Contributed by @BillCarsonFr.
|
||||
* Tweak rendering of icons for a11y ([#31358](https://github.com/element-hq/element-web/pull/31358)). Contributed by @t3chguy.
|
||||
* Implement new `renderNotificationDecoration` from module API ([#31389](https://github.com/element-hq/element-web/pull/31389)). Contributed by @MidhunSureshR.
|
||||
* Replace more icons with compound ([#31381](https://github.com/element-hq/element-web/pull/31381)). Contributed by @t3chguy.
|
||||
* Replace more icons with compound ([#31378](https://github.com/element-hq/element-web/pull/31378)). Contributed by @t3chguy.
|
||||
* `<Banner/>`: Hide `Dismiss` button if `onClose` handler is not provided. ([#31362](https://github.com/element-hq/element-web/pull/31362)). Contributed by @kaylendog.
|
||||
* Replace batch of legacy icons with compound design tokens ([#31360](https://github.com/element-hq/element-web/pull/31360)). Contributed by @t3chguy.
|
||||
* MSC4380: Invite blocking ([#31268](https://github.com/element-hq/element-web/pull/31268)). Contributed by @richvdh.
|
||||
* Tweak rendering of icons for accessibility ([#31346](https://github.com/element-hq/element-web/pull/31346)). Contributed by @t3chguy.
|
||||
* Implement a shared `Banner` component. ([#31266](https://github.com/element-hq/element-web/pull/31266)). Contributed by @kaylendog.
|
||||
* Allow the Login screen to use the dark theme ([#31293](https://github.com/element-hq/element-web/pull/31293)). Contributed by @richvdh.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* [Backport staging] Amend e2e normal icon from lock-solid to info ([#31559](https://github.com/element-hq/element-web/pull/31559)). Contributed by @t3chguy.
|
||||
* [Backport staging] Fix CSS specificity causing icon issues in e2e verification ([#31548](https://github.com/element-hq/element-web/pull/31548)). Contributed by @RiotRobot.
|
||||
* [Backport staging] Fix e2e icons in CompleteSecurity \& SetupEncryptionBody ([#31522](https://github.com/element-hq/element-web/pull/31522)). Contributed by @RiotRobot.
|
||||
* [Backport staging] Remove an extra paragraph in advanced room settings ([#31511](https://github.com/element-hq/element-web/pull/31511)). Contributed by @RiotRobot.
|
||||
* [Backport staging] Don't show the key storage out of sync toast when backup disabled ([#31507](https://github.com/element-hq/element-web/pull/31507)). Contributed by @RiotRobot.
|
||||
* Fix composer button visibility in contrast colour mode ([#31255](https://github.com/element-hq/element-web/pull/31255)). Contributed by @t3chguy.
|
||||
* Ensure correct room version is used and permissions are appropriately sert when creating rooms ([#31464](https://github.com/element-hq/element-web/pull/31464)). Contributed by @Half-Shot.
|
||||
* Fix e2e icon rendering ([#31454](https://github.com/element-hq/element-web/pull/31454)). Contributed by @t3chguy.
|
||||
* EventIndexer: ensure we add initial checkpoints when the db is first opened ([#31448](https://github.com/element-hq/element-web/pull/31448)). Contributed by @richvdh.
|
||||
* Fix `/join <alias>` command failing due to race condition ([#31433](https://github.com/element-hq/element-web/pull/31433)). Contributed by @MidhunSureshR.
|
||||
* MessageEventIndexDialog: distinguish indexed rooms ([#31436](https://github.com/element-hq/element-web/pull/31436)). Contributed by @richvdh.
|
||||
* Move `EditInPlace` out of `Form` (Fixes: reloading EW on EC url update) ([#31434](https://github.com/element-hq/element-web/pull/31434)). Contributed by @toger5.
|
||||
* Fixes issue where cursor would jump to the beginning of the input field after converting Japanese text and pressing Tab ([#31432](https://github.com/element-hq/element-web/pull/31432)). Contributed by @shinaoka.
|
||||
* Fix widgets getting stuck in loading states ([#31314](https://github.com/element-hq/element-web/pull/31314)). Contributed by @robintown.
|
||||
* Room list: fix room options remaining on room item after mouse leaving ([#31414](https://github.com/element-hq/element-web/pull/31414)). Contributed by @florianduros.
|
||||
* Make `RoomList.showMessagePreview` configurable by `config.json` ([#31419](https://github.com/element-hq/element-web/pull/31419)). Contributed by @florianduros.
|
||||
* Fix bug which caused app not to load correctly when `force_verification` is enabled ([#31265](https://github.com/element-hq/element-web/pull/31265)). Contributed by @richvdh.
|
||||
* Room list: display the menu option on the room list item when clicked/opened ([#31380](https://github.com/element-hq/element-web/pull/31380)). Contributed by @florianduros.
|
||||
* Fix handling of SVGs ([#31359](https://github.com/element-hq/element-web/pull/31359)). Contributed by @t3chguy.
|
||||
* Fix word wrapping in expanded left panel buttons ([#31377](https://github.com/element-hq/element-web/pull/31377)). Contributed by @t3chguy.
|
||||
* Fix aspect ratio on error view background ([#31361](https://github.com/element-hq/element-web/pull/31361)). Contributed by @t3chguy.
|
||||
* Fix failure to request persistent storage perms ([#31299](https://github.com/element-hq/element-web/pull/31299)). Contributed by @richvdh.
|
||||
* Fix calls sometimes not knowing that they're presented ([#31313](https://github.com/element-hq/element-web/pull/31313)). Contributed by @robintown.
|
||||
|
||||
|
||||
Changes in [1.12.6](https://github.com/element-hq/element-web/releases/tag/v1.12.6) (2025-12-03)
|
||||
================================================================================================
|
||||
This release fixes a bug where 1:1 calling was incorrectly not available if no Element Call focus was set.
|
||||
|
|
|
|||
|
|
@ -43,7 +43,6 @@
|
|||
},
|
||||
"element_call": {
|
||||
"url": "https://call.element.io",
|
||||
"participant_limit": 8,
|
||||
"brand": "Element Call"
|
||||
},
|
||||
"map_style_url": "https://api.maptiler.com/maps/streets/style.json?key=fU3vlMsMn4Jb6dnEIFsx"
|
||||
|
|
|
|||
|
|
@ -391,8 +391,6 @@ The VoIP and Jitsi options are:
|
|||
6. `element_call`: Optional configuration for native group calls using Element Call, with the following subkeys:
|
||||
- `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`.
|
||||
- `participant_limit`: The maximum number of users who can join a call; if
|
||||
this number is exceeded, the user will not be able to join a given call.
|
||||
- `brand`: Optional name for the app. Defaults to `Element Call`. This is
|
||||
used throughout the application in various strings/locations.
|
||||
- `guest_spa_url`: Optional URL for an Element Call single-page app (SPA),
|
||||
|
|
@ -588,6 +586,22 @@ Currently, the following UI feature flags are supported:
|
|||
- `UIFeature.allowCreatingPublicRooms` - Whether or not public rooms can be created.
|
||||
- `UIFeature.allowCreatingPublicSpaces` - Whether or not public spaces can be created.
|
||||
|
||||
## Modules
|
||||
|
||||
`modules`: An optional array of module paths to load at runtime. Each entry is a URL or path to a JavaScript module entry point that will be dynamically imported when Element Web starts.
|
||||
|
||||
**Note:** This is separate from the build-time module system configured via `build_config.yaml`. Runtime modules are loaded dynamically from the paths specified in `config.json`, while build-time modules are bundled during compilation.
|
||||
|
||||
**Example:**
|
||||
|
||||
```json
|
||||
{
|
||||
"modules": ["https://example.com/my-module.js", "/path/to/local-module.js"]
|
||||
}
|
||||
```
|
||||
|
||||
Each module URL is loaded using dynamic import (`import()`). The modules are loaded in order after Element Web initializes but before the application fully starts. Modules must be accessible from the browser and should export a compatible module format that works with the [Module API](https://github.com/element-hq/element-modules/tree/main/packages/element-web-module-api).
|
||||
|
||||
## Undocumented / developer options
|
||||
|
||||
The following are undocumented or intended for developer use only.
|
||||
|
|
@ -596,4 +610,3 @@ The following are undocumented or intended for developer use only.
|
|||
2. `sync_timeline_limit`
|
||||
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.
|
||||
5. `modules`: An optional list of modules to load. This is used for testing and development purposes only.
|
||||
|
|
|
|||
22
docs/labs.md
|
|
@ -112,3 +112,25 @@ 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.
|
||||
|
||||
## Exclude insecure devices when sending/receiving messages (`feature_exclude_insecure_devices`)
|
||||
|
||||
Do not send or receive messages to/from devices that are not properly verified. Users with unverified devices will not
|
||||
receive your messages at all on those devices, and if they send messages, you will not be able to read them, but you
|
||||
will be aware that a message exists.
|
||||
|
||||
## Share encrypted history with new members (`feature_share_history_on_invite`) [In Development]
|
||||
|
||||
When inviting users to an encrypted room with shared history (i.e. a room with the "Who can read history?" setting set
|
||||
to "Members only (since the point in time of selecting this option)"), send the keys for previous messages to the
|
||||
invitee so they can read them.
|
||||
|
||||
Both the inviter and the invitee must set this labs flag, before the invitation is sent.
|
||||
|
||||
## Encrypted state events (MSC4362) (`feature_msc4362_encrypted_state_events`)
|
||||
|
||||
Encrypt most of the state events in the room, including the room name and topic.
|
||||
|
||||
WARNING: this means that users joining a room who do not have access to its history will not be able to see the name or
|
||||
topic of the room, or any other room state information. It also means the room name and topic are not available before
|
||||
joining a room.
|
||||
|
|
|
|||
|
|
@ -2,216 +2,485 @@
|
|||
|
||||
## Contents
|
||||
|
||||
- How to run the tests
|
||||
- How the tests work
|
||||
- How to write great Playwright tests
|
||||
- Visual testing
|
||||
- [Overview](#overview)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Running the Tests](#running-the-tests)
|
||||
- [Element Web E2E Tests](#element-web-e2e-tests)
|
||||
- [Shared Components Tests](#shared-components-tests)
|
||||
- [Projects](#projects)
|
||||
- [How the Tests Work](#how-the-tests-work)
|
||||
- [Test Structure](#test-structure)
|
||||
- [Homeserver Setup](#homeserver-setup)
|
||||
- [Fixtures](#fixtures)
|
||||
- [Writing Tests](#writing-tests)
|
||||
- [Getting a Homeserver](#getting-a-homeserver)
|
||||
- [Logging In](#logging-in)
|
||||
- [Joining a Room](#joining-a-room)
|
||||
- [Using matrix-js-sdk](#using-matrix-js-sdk)
|
||||
- [Best Practices](#best-practices)
|
||||
- [Visual Testing](#visual-testing)
|
||||
- [Test Tags](#test-tags)
|
||||
- [Supported Container Runtimes](#supported-container-runtimes)
|
||||
|
||||
## Overview
|
||||
|
||||
Element Web contains two sets of Playwright tests:
|
||||
|
||||
1. **Element Web E2E Tests** (`playwright/e2e/`) - Full end-to-end tests of the Element Web application with real homeserver instances
|
||||
2. **Shared Components Tests** (`packages/shared-components/`) - Visual regression tests for the shared component library using Storybook
|
||||
|
||||
Both test suites run automatically in CI on every pull request and on every merge to develop & master.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before running Playwright tests, ensure you have the following set up:
|
||||
|
||||
### 1. Install Playwright Browsers and System Dependencies
|
||||
|
||||
Follow the Playwright installation instructions:
|
||||
|
||||
- **Browsers:** <https://playwright.dev/docs/browsers#install-browsers>
|
||||
- **System dependencies:** <https://playwright.dev/docs/browsers#install-system-dependencies>
|
||||
|
||||
```sh
|
||||
yarn playwright install --with-deps
|
||||
```
|
||||
|
||||
### 2. Container Runtime
|
||||
|
||||
See [Supported Container Runtimes](#supported-container-runtimes) for details on supported container runtimes (Docker, Podman, Colima).
|
||||
|
||||
### 3. Element Web Server (for E2E tests)
|
||||
|
||||
Element Web E2E tests require an instance running on `http://localhost:8080` (configured in `playwright.config.ts`).
|
||||
|
||||
You can either:
|
||||
|
||||
- **Run manually:** `yarn start` in a separate terminal (not working for screenshot tests running in a docker environment).
|
||||
- **Auto-start:** Playwright will start the webserver automatically if it's not already running
|
||||
|
||||
## Running the Tests
|
||||
|
||||
Our Playwright tests run automatically as part of our CI along with our other tests,
|
||||
on every pull request and on every merge to develop & master.
|
||||
### Element Web E2E Tests
|
||||
|
||||
You may need to follow instructions to set up your development environment for running
|
||||
Playwright by following <https://playwright.dev/docs/browsers#install-browsers> and
|
||||
<https://playwright.dev/docs/browsers#install-system-dependencies>.
|
||||
Our main Playwright tests run against a full Element Web instance with Synapse/Dendrite homeservers.
|
||||
|
||||
However the Playwright tests are run, an element-web instance must be running on
|
||||
http://localhost:8080 (this is configured in `playwright.config.ts`) - this is what will
|
||||
be tested. When running Playwright tests yourself, the standard `yarn start` from the
|
||||
element-web project is fine: leave it running it a different terminal as you would
|
||||
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.
|
||||
**Run all E2E tests:**
|
||||
|
||||
The tests use [testcontainers](https://node.testcontainers.org/) to launch Homeserver (Synapse or Dendrite)
|
||||
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:
|
||||
|
||||
```shell
|
||||
```sh
|
||||
yarn run test:playwright
|
||||
```
|
||||
|
||||
This will run the Playwright tests once, non-interactively.
|
||||
**Run a specific test file:**
|
||||
|
||||
You can also run individual tests this way too, as you'd expect:
|
||||
|
||||
```shell
|
||||
yarn run test:playwright --spec playwright/e2e/register/register.spec.ts
|
||||
```sh
|
||||
yarn run test:playwright playwright/e2e/register/register.spec.ts
|
||||
```
|
||||
|
||||
Playwright also has its own UI that you can use to run and debug the tests.
|
||||
To launch it:
|
||||
**Run tests interactively with Playwright UI:**
|
||||
|
||||
```shell
|
||||
yarn run test:playwright:open --headed --debug
|
||||
```sh
|
||||
yarn run test:playwright:open
|
||||
```
|
||||
|
||||
See more command line options at <https://playwright.dev/docs/test-cli>.
|
||||
**Run screenshot tests only:**
|
||||
|
||||
## Projects
|
||||
> [!WARNING]
|
||||
> This command run the playwright tests in a docker environment.
|
||||
|
||||
By default, Playwright will run all "Projects", this means tests will run against Chrome, Firefox and "Safari" (Webkit).
|
||||
We only run tests against Chrome in pull request CI, but all projects in the merge queue.
|
||||
Some tests are excluded from running on certain browsers due to incompatibilities in the test harness.
|
||||
```sh
|
||||
yarn run test:playwright:screenshots
|
||||
```
|
||||
|
||||
For more information about visual testing, see [Visual Testing](playwright#visual-testing).
|
||||
|
||||
**Additional command line options:** <https://playwright.dev/docs/test-cli>
|
||||
|
||||
### Shared Components Tests
|
||||
|
||||
The shared-components package uses Playwright (via Storybook test runner) to validate component rendering across different states and configurations.
|
||||
|
||||
**Run Storybook tests:**
|
||||
|
||||
```sh
|
||||
cd packages/shared-components
|
||||
yarn test:storybook
|
||||
```
|
||||
|
||||
**Run Storybook tests in CI mode:**
|
||||
|
||||
```sh
|
||||
cd packages/shared-components
|
||||
yarn test:storybook:ci
|
||||
```
|
||||
|
||||
**Update Storybook screenshots:**
|
||||
|
||||
```sh
|
||||
cd packages/shared-components
|
||||
yarn test:storybook:update
|
||||
```
|
||||
|
||||
This uses the same Docker-based screenshot rendering as Element Web to ensure consistency across platforms.
|
||||
|
||||
### Projects
|
||||
|
||||
By default, Playwright runs tests against all "Projects": Chrome, Firefox, "Safari" (Webkit), Dendrite and Picone.
|
||||
|
||||
- Chrome, Firefox, Safari run against Synapse
|
||||
- Dendrite and Picone run against Chrome
|
||||
|
||||
Misc:
|
||||
|
||||
- **Pull Request CI:** Tests run only against Chrome
|
||||
- **Merge Queue:** Tests run against all projects
|
||||
- Some tests are excluded from certain browsers due to incompatibilities (see [Test Tags](#test-tags))
|
||||
|
||||
## How the Tests Work
|
||||
|
||||
Everything Playwright-related lives in the `playwright/` subdirectory
|
||||
as is typical for Playwright tests. Likewise, tests live in `playwright/e2e`.
|
||||
### Test Structure
|
||||
|
||||
`playwright/testcontainers` contains the testcontainers which start instances
|
||||
of Synapse/Dendrite. These servers are what Element-web runs against in the tests.
|
||||
**Element Web tests** are located in the `playwright/` subdirectory:
|
||||
|
||||
Synapse can be launched with different configurations in order to test element
|
||||
in different configurations. You can specify `synapseConfig` as such:
|
||||
- `playwright/e2e/` - E2E test files
|
||||
- `playwright/testcontainers/` - Testcontainers for Synapse/Dendrite instances
|
||||
- `playwright/snapshots/` - Visual regression test screenshots
|
||||
- `playwright/pages/` - Page object models
|
||||
- `playwright/plugins/` - Custom Playwright plugins
|
||||
|
||||
**Shared components tests** are located in `packages/shared-components/`:
|
||||
|
||||
- `packages/shared-components/playwright/snapshots/` - Storybook screenshot baselines
|
||||
- `packages/shared-components/.storybook/` - Storybook configuration
|
||||
|
||||
The shared components use Storybook's test runner (powered by Playwright) to validate component rendering across different states and configurations.
|
||||
|
||||
### Homeserver Setup
|
||||
|
||||
Homeservers (Synapse or Dendrite) are launched by Playwright workers and reused for all tests matching the worker configuration.
|
||||
|
||||
**Configure Synapse options:**
|
||||
|
||||
```typescript
|
||||
test.use({
|
||||
synapseConfig: {
|
||||
// The config options to pass to the Synapse instance
|
||||
// Configuration options for the Synapse instance
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
The appropriate homeserver will be launched by the Playwright worker and reused for all tests which match the worker configuration.
|
||||
Due to homeservers being reused between tests, please use unique names for any rooms put into the room directory as
|
||||
they may be visible from other tests, the suggested approach is to use `testInfo.testId` within the name or lodash's uniqueId.
|
||||
We remove public rooms from the room directory between tests but deleting users doesn't have a homeserver agnostic solution.
|
||||
The logs from testcontainers will be attached to any reports output from Playwright.
|
||||
**Important notes:**
|
||||
|
||||
- Homeservers are reused between tests for efficiency
|
||||
- Please use unique names for any rooms put into the room directory as they may be visible from other tests, the suggested approach is to use `testInfo.testId` within the name or lodash's uniqueId.
|
||||
- We remove public rooms from the room directory between tests but deleting users doesn't have a homeserver agnostic solution.
|
||||
- Homeserver logs are attached to Playwright test reports
|
||||
|
||||
### Fixtures
|
||||
|
||||
We heavily leverage [Playwright fixtures](https://playwright.dev/docs/test-fixtures) to provide:
|
||||
|
||||
- Homeserver instances (`homeserver`)
|
||||
- Logged-in users (`user`)
|
||||
- Bot users (`bot`)
|
||||
- Application state (`app`)
|
||||
|
||||
See [Writing Tests](#writing-tests) for usage examples.
|
||||
|
||||
## Writing Tests
|
||||
|
||||
Mostly this is the same advice as for writing any other Playwright test: the Playwright
|
||||
docs are well worth a read if you're not already familiar with Playwright testing, eg.
|
||||
https://playwright.dev/docs/best-practices. To avoid your tests being flaky it is also
|
||||
recommended to use [auto-retrying assertions](https://playwright.dev/docs/test-assertions#auto-retrying-assertions).
|
||||
For general Playwright best practices, see:
|
||||
|
||||
### Getting a Synapse
|
||||
- <https://playwright.dev/docs/best-practices>
|
||||
- <https://playwright.dev/docs/test-assertions#auto-retrying-assertions> (recommended for avoiding flaky tests)
|
||||
|
||||
We heavily leverage the magic of [Playwright fixtures](https://playwright.dev/docs/test-fixtures).
|
||||
To acquire a homeserver within a test just add the `homeserver` fixture to the test:
|
||||
### Getting a Homeserver
|
||||
|
||||
Use the `homeserver` fixture to acquire a Homeserver instance:
|
||||
|
||||
```typescript
|
||||
test("should do something", async ({ homeserver }) => {
|
||||
// homeserver is a Synapse/Dendrite instance
|
||||
// homeserver is a ready-to-use Synapse/Dendrite instance
|
||||
});
|
||||
```
|
||||
|
||||
This returns an object with information about the Homeserver instance, including what port
|
||||
it was started on and the ID that needs to be passed to shut it down again. It also
|
||||
returns the registration shared secret (`registrationSecret`) that can be used to
|
||||
register users via the REST API. The Homeserver has been ensured ready to go by awaiting
|
||||
its internal health-check.
|
||||
**The fixture provides:**
|
||||
|
||||
Homeserver instances should be reasonably cheap to start (you may see the first one take a
|
||||
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.
|
||||
- Server port information
|
||||
- Instance ID for shutdown
|
||||
- Registration shared secret (`registrationSecret`) for registering users via REST API
|
||||
|
||||
Homeserver instances are:
|
||||
|
||||
- Reasonably cheap to start (first run may be slow while pulling Docker image)
|
||||
- Automatically cleaned up by the fixture
|
||||
|
||||
### Logging In
|
||||
|
||||
We again heavily leverage the magic of [Playwright fixtures](https://playwright.dev/docs/test-fixtures).
|
||||
To acquire a logged-in user within a test just add the `user` fixture to the test:
|
||||
Use the `user` fixture to get a logged-in user:
|
||||
|
||||
```typescript
|
||||
test("should do something", async ({ user }) => {
|
||||
// user is a logged in user
|
||||
// user is logged in and ready to use
|
||||
});
|
||||
```
|
||||
|
||||
You can specify a display name for the user via `test.use` `displayName`,
|
||||
otherwise a random one will be generated.
|
||||
This will register a random userId using the registrationSecret with a random password
|
||||
and the given display name. The user fixture will contain details about the credentials for if
|
||||
they are needed for User-Interactive Auth or similar but localStorage will already be seeded with them
|
||||
and the app loaded (path `/`).
|
||||
**Customize the user:**
|
||||
|
||||
```typescript
|
||||
test.use({
|
||||
displayName: "Alice",
|
||||
});
|
||||
|
||||
test("should do something", async ({ user }) => {
|
||||
// user is logged in as "Alice"
|
||||
});
|
||||
```
|
||||
|
||||
**What the fixture does:**
|
||||
|
||||
- Registers a random userId with the `registrationSecret`
|
||||
- Generates a random password (or uses specified display name)
|
||||
- Seeds localStorage with credentials
|
||||
- Loads the app at path `/`
|
||||
- Provides user details for User-Interactive Auth if needed
|
||||
|
||||
### Joining a Room
|
||||
|
||||
Many tests will also want to start with the client in a room, ready to send & receive messages. Best
|
||||
way to do this may be to get an access token for the user and use this to create a room with the REST
|
||||
API before logging the user in.
|
||||
You can make use of the bot fixture and the `client` field on the app fixture to do this.
|
||||
To start with a user in a room:
|
||||
|
||||
### Try to write tests from the users' perspective
|
||||
```typescript
|
||||
test("should send a message", async ({ user, app, bot }) => {
|
||||
// Use the bot client to create a room
|
||||
const roomId = await bot.createRoom({
|
||||
name: "Test Room",
|
||||
invite: [user.userId],
|
||||
});
|
||||
|
||||
Like for instance a user will not look for a button by querying a CSS selector.
|
||||
Instead, you should work with roles / labels etc, see https://playwright.dev/docs/locators.
|
||||
// Accept the invite using the app client
|
||||
await app.client.joinRoom(roomId);
|
||||
|
||||
// Now ready to test messaging
|
||||
});
|
||||
```
|
||||
|
||||
**Best practice:** Use the REST API (via `bot` or `app.client`) to set up room state rather than driving the UI.
|
||||
|
||||
### Using matrix-js-sdk
|
||||
|
||||
Due to the way we run the Playwright tests in CI, at this time you can only use the matrix-js-sdk module
|
||||
exposed on `window.matrixcs`. This has the limitation that it is only accessible with the app loaded.
|
||||
This may be revisited in the future.
|
||||
Due to CI constraints, use the matrix-js-sdk module exposed on `window.matrixcs`:
|
||||
|
||||
## Good Test Hygiene
|
||||
```typescript
|
||||
const matrixcs = window.matrixcs;
|
||||
```
|
||||
|
||||
This section mostly summarises general good Playwright testing practice, and should not be news to anyone
|
||||
already familiar with Playwright.
|
||||
**Limitation:** Only accessible when the app is loaded. This may be revisited in the future.
|
||||
|
||||
1. Test a well-isolated unit of functionality. The more specific, the easier it will be to tell what's
|
||||
wrong when they fail.
|
||||
1. Don't depend on state from other tests: any given test should be able to run in isolation.
|
||||
1. Try to avoid driving the UI for anything other than the UI you're trying to test. e.g. if you're
|
||||
testing that the user can send a reaction to a message, it's best to send a message using a REST
|
||||
API, then react to it using the UI, rather than using the element-web UI to send the message.
|
||||
1. Avoid explicit waits. Playwright locators & assertions will implicitly wait for the specified
|
||||
element to appear and all assertions are retried until they either pass or time out, so you should
|
||||
never need to manually wait for an element.
|
||||
- For example, for asserting about editing an already-edited message, you can't wait for the
|
||||
'edited' element to appear as there was already one there, but you can assert that the body
|
||||
of the message is what is should be after the second edit and this assertion will pass once
|
||||
it becomes true. You can then assert that the 'edited' element is still in the DOM.
|
||||
- You can also wait for other things like network requests in the
|
||||
browser to complete (https://playwright.dev/docs/api/class-page#page-wait-for-response).
|
||||
Needing to wait for things can also be because of race conditions in the app itself, which ideally
|
||||
shouldn't be there!
|
||||
### Best Practices
|
||||
|
||||
This is a small selection - the Playwright best practices guide, linked above, has more good advice, and we
|
||||
should generally try to adhere to them.
|
||||
For more guidance, see the [Playwright best practices guide](https://playwright.dev/docs/best-practices).
|
||||
|
||||
## Screenshot testing
|
||||
#### 1. Test from the User's Perspective
|
||||
|
||||
When we previously used Cypress we also dabbled with Percy, and whilst powerful it did not
|
||||
lend itself well to being executed on all PRs without needing to budget it substantially.
|
||||
Work with roles, labels, and accessible elements rather than CSS selectors:
|
||||
|
||||
```typescript
|
||||
// Good
|
||||
await page.getByRole("button", { name: "Send" }).click();
|
||||
|
||||
// Avoid
|
||||
await page.locator(".mx_MessageComposer_sendButton").click();
|
||||
```
|
||||
|
||||
See <https://playwright.dev/docs/locators> for more guidance.
|
||||
|
||||
#### 2. Test Well-Isolated Functionality
|
||||
|
||||
- Focus on specific, well-defined units of functionality
|
||||
- Easier to debug when tests fail
|
||||
- More maintainable over time
|
||||
|
||||
#### 3. Maintain Test Independence
|
||||
|
||||
- Each test should run successfully in isolation
|
||||
- Don't depend on state from other tests
|
||||
- Clean up after your test if needed
|
||||
|
||||
#### 4. Minimize UI Driving for Setup
|
||||
|
||||
- Use REST APIs to set up test state when possible
|
||||
- Only drive the UI for the functionality you're actually testing
|
||||
|
||||
**Example:**
|
||||
|
||||
```typescript
|
||||
// Testing reactions - good approach
|
||||
test("should react to a message", async ({ page, app, bot }) => {
|
||||
// Send message via API
|
||||
const eventId = await bot.sendMessage(roomId, "Hello");
|
||||
|
||||
// Test the reaction UI
|
||||
await page.getByText("Hello").hover();
|
||||
await page.getByRole("button", { name: "React" }).click();
|
||||
await page.getByLabel("😀").click();
|
||||
|
||||
// Verify reaction was sent
|
||||
await expect(page.getByLabel("😀 1")).toBeVisible();
|
||||
});
|
||||
```
|
||||
|
||||
#### 5. Avoid Explicit Waits
|
||||
|
||||
Playwright locators and assertions automatically wait and retry:
|
||||
|
||||
```typescript
|
||||
// Good - implicit waiting
|
||||
await expect(page.getByText("Message sent")).toBeVisible();
|
||||
|
||||
// Avoid - explicit waits
|
||||
await page.waitForTimeout(1000);
|
||||
```
|
||||
|
||||
**For dynamic content:**
|
||||
|
||||
```typescript
|
||||
// Assert on the final state - Playwright will wait for it
|
||||
await expect(page.getByRole("textbox")).toHaveValue("Edited message");
|
||||
await expect(page.getByText("edited")).toBeVisible();
|
||||
```
|
||||
|
||||
**When you do need to wait:**
|
||||
|
||||
```typescript
|
||||
// Wait for network requests
|
||||
await page.waitForResponse("**/messages");
|
||||
|
||||
// Wait for specific conditions
|
||||
await page.waitForFunction(() => window.matrixcs !== undefined);
|
||||
```
|
||||
|
||||
## Visual Testing
|
||||
|
||||
Playwright has built-in support for [visual comparison testing](https://playwright.dev/docs/test-snapshots).
|
||||
Screenshots are saved in `playwright/snapshots` and are rendered in a Linux Docker environment for stability.
|
||||
|
||||
One must be careful to exclude any dynamic content from the screenshot, such as timestamps, avatars, etc,
|
||||
via the `mask` option. See the [Playwright docs](https://playwright.dev/docs/test-snapshots#masking).
|
||||
**Screenshot location:** `playwright/snapshots/`
|
||||
|
||||
Some UI elements render differently between test runs, such as BaseAvatar when
|
||||
there is no avatar set, choosing a colour from the theme palette based on the
|
||||
hash of the user/room's Matrix ID. To avoid this creating flaky tests we inject
|
||||
some custom CSS, for this to happen we use the custom assertion `toMatchScreenshot`
|
||||
instead of the native `toHaveScreenshot`.
|
||||
**Rendering environment:** Linux Docker (for consistency across environments)
|
||||
|
||||
If you are running Linux and are unfortunate that the screenshots are not rendering identically,
|
||||
you may wish to specify `--ignore-snapshots` and rely on Docker to render them for you.
|
||||
### Test Tag for Screenshots
|
||||
|
||||
All screenshot tests must use the `@screenshot` tag:
|
||||
|
||||
```typescript
|
||||
test("should render message list", { tag: "@screenshot" }, async ({ page }) => {
|
||||
await expect(page).toMatchScreenshot("message-list.png");
|
||||
});
|
||||
```
|
||||
|
||||
**Purpose of `@screenshot` tag:**
|
||||
|
||||
- Allows running only screenshot tests via `test:playwright:screenshots`
|
||||
- Speeds up screenshot test runs and updates
|
||||
|
||||
### Taking Screenshots
|
||||
|
||||
Use the custom `toMatchScreenshot` assertion (not the native `toHaveScreenshot`):
|
||||
|
||||
```typescript
|
||||
await expect(page).toMatchScreenshot("my-screenshot.png");
|
||||
```
|
||||
|
||||
**Why a custom assertion?** We inject custom CSS to stabilize dynamic UI elements (e.g., BaseAvatar color selection based on Matrix ID hash).
|
||||
|
||||
### Masking Dynamic Content
|
||||
|
||||
Always mask dynamic content that changes between runs:
|
||||
|
||||
```typescript
|
||||
await expect(page).toMatchScreenshot("chat.png", {
|
||||
mask: [page.locator(".mx_MessageTimestamp"), page.locator(".mx_BaseAvatar")],
|
||||
});
|
||||
```
|
||||
|
||||
Common elements to mask:
|
||||
|
||||
- Timestamps
|
||||
- Avatars (when dynamic)
|
||||
- Animated elements
|
||||
- User-generated IDs
|
||||
|
||||
See [Playwright masking docs](https://playwright.dev/docs/test-snapshots#masking) for more details.
|
||||
|
||||
### Updating Screenshots
|
||||
|
||||
This command runs only tests tagged with `@screenshot` in the Docker environment.
|
||||
When you need to update screenshot baselines (e.g., after intentional UI changes):
|
||||
|
||||
```sh
|
||||
yarn run test:playwright:screenshots
|
||||
```
|
||||
|
||||
**Important:** Always use this command to update screenshots rather than running tests locally with `--update-snapshots`.
|
||||
|
||||
**Why?** Screenshots must be rendered in a consistent Linux Docker environment because:
|
||||
|
||||
- Font rendering differs between operating systems (macOS, Windows, Linux)
|
||||
- Subpixel rendering varies across systems
|
||||
- Browser rendering engines have platform-specific differences
|
||||
|
||||
Using `test:playwright:screenshots` ensures screenshots are generated in the same Docker environment used in CI, preventing false failures due to rendering differences.
|
||||
|
||||
## Test Tags
|
||||
|
||||
We use test tags to categorise tests for running subsets more efficiently.
|
||||
Test tags categorize tests for efficient subset execution.
|
||||
|
||||
- `@mergequeue`: Tests that are slow or flaky and cover areas of the app we update seldom, should not be run on every PR commit but will be run in the Merge Queue.
|
||||
- `@screenshot`: Tests that use `toMatchScreenshot` to speed up a run of `test:playwright:screenshots`. A test with this tag must not also have the `@mergequeue` tag as this would cause false positives in the stale screenshot detection.
|
||||
- `@no-$project`: Tests which are unsupported in $Project. These tests will be skipped when running in $Project.
|
||||
### Available Tags
|
||||
|
||||
Anything testing Matrix media will need to have `@no-firefox` and `@no-webkit` as those rely on the service worker which
|
||||
has to be disabled in Playwright on Firefox & Webkit to retain routing functionality.
|
||||
Anything testing VoIP/microphone will need to have `@no-webkit` as fake microphone functionality is not available
|
||||
there at this time.
|
||||
- **`@mergequeue`**: Slow or flaky tests covering rarely-updated app areas
|
||||
- Not run on every PR commit
|
||||
- Run in the Merge Queue
|
||||
|
||||
If you wish to run all tests in a PR, you can give it the label `X-Run-All-Tests`.
|
||||
- **`@screenshot`**: Tests using `toMatchScreenshot` for visual regression testing
|
||||
- See the [Visual Testing](#visual-testing) section for detailed usage
|
||||
|
||||
## Supporter container runtimes
|
||||
- **`@no-firefox`**: Tests unsupported in Firefox
|
||||
- Automatically skipped in Firefox project
|
||||
- Common reason: Service worker required (disabled in Playwright Firefox for routing)
|
||||
|
||||
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/
|
||||
- **`@no-webkit`**: Tests unsupported in Webkit
|
||||
- Automatically skipped in Webkit project
|
||||
- Common reasons: Service worker required, microphone functionality unavailable
|
||||
|
||||
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.
|
||||
### Running All Tests in a PR
|
||||
|
||||
Add the `X-Run-All-Tests` label to your pull request to run all tests, including `@mergequeue` tests.
|
||||
|
||||
## Supported Container Runtimes
|
||||
|
||||
We use [testcontainers](https://node.testcontainers.org/) to manage Synapse, Matrix Authentication Service, and other service instances.
|
||||
|
||||
**Supported runtimes:**
|
||||
|
||||
- Docker (default, recommended)
|
||||
- Podman
|
||||
- Colima
|
||||
See setup instructions: <https://node.testcontainers.org/supported-container-runtimes/>
|
||||
|
||||
### Platform-Specific Configuration
|
||||
|
||||
**Colima users:**
|
||||
|
||||
If using Colima, you may need to set the `TMPDIR` environment variable to allow bind mounting temporary directories:
|
||||
|
||||
```sh
|
||||
export TMPDIR=/tmp/colima
|
||||
# or
|
||||
export TMPDIR=$HOME/tmp
|
||||
```
|
||||
|
||||
**macOS users:**
|
||||
|
||||
Docker Desktop and Colima are both well-supported on macOS.
|
||||
|
||||
> [!CAUTION]
|
||||
> Do not set `DOCKER_HOST` when running tests. Element Web uses [element-web-playwright-common](https://github.com/element-hq/element-modules/tree/main/packages/element-web-playwright-common), and setting `DOCKER_HOST` causes issues with testcontainers when running in the container VM.
|
||||
|
|
|
|||
|
|
@ -58,6 +58,12 @@ We are aiming for a set of common strings to be shared then some more localised
|
|||
Edits to existing strings should be performed only via Localazy.
|
||||
There you can also require all translations to be redone if the meaning of the string has changed significantly.
|
||||
|
||||
## Removing existing strings
|
||||
|
||||
You can remove an existing string by removing the key from `en_EN.json`. Do not modify other language json files for this purpose.
|
||||
|
||||
Localazy will mark the keys you removed as deprecated. See https://localazy.com/docs/general/editing-source-language#source-key-states for more information about the difference between deprecated keys and deleted keys.
|
||||
|
||||
## Adding variables inside a string.
|
||||
|
||||
1. Extend your `_t()` call. Instead of `_t(TKEY)` use `_t(TKEY, {})`
|
||||
|
|
@ -66,7 +72,7 @@ There you can also require all translations to be redone if the meaning of the s
|
|||
1. Add the variable inside the string. The syntax for variables is `%(variable)s`. Please note the _s_ at the end. The name of the variable has to match the previous used name.
|
||||
|
||||
- You can use the special `count` variable to choose between multiple versions of the same string, in order to get the correct pluralization. E.g. `_t('You have %(count)s new messages', { count: 2 })` would show 'You have 2 new messages', while `_t('You have %(count)s new messages', { count: 1 })` would show 'You have one new message' (assuming a singular version of the string has been added to the translation file. See above). Passing in `count` is much preferred over having an if-statement choose the correct string to use, because some languages have much more complicated plural rules than english (e.g. they might need a completely different form if there are three things rather than two).
|
||||
- If you want to translate text that includes e.g. hyperlinks or other HTML you have to also use tag substitution, e.g. `_t('<a>Click here!</a>', {}, { 'a': (sub) => <a>{sub}</a> })`. If you don't do the tag substitution you will end up showing literally '<a>' rather than making a hyperlink.
|
||||
- If you want to translate text that includes e.g. hyperlinks or other HTML you have to also use tag substitution, e.g. `_t('<a>Click here!</a>', {}, { 'a': (sub) => <a>{sub}</a> })`. If you don't do the tag substitution you will end up showing literally `<a>` rather than making a hyperlink.
|
||||
- You can also use React components with normal variable substitution if you want to insert HTML markup, e.g. `_t('Your email address is %(emailAddress)s', { emailAddress: <i>{userEmailAddress}</i> })`.
|
||||
|
||||
## Things to know/Style Guides
|
||||
|
|
@ -78,4 +84,4 @@ There you can also require all translations to be redone if the meaning of the s
|
|||
- Concatenating strings often also introduces an implicit assumption about word order (e.g. that the subject of the sentence comes first), which is incorrect for many languages.
|
||||
- Translation 'smell test': If you have a string that does not begin with a capital letter (is not the start of a sentence) or it ends with e.g. ':' or a preposition (e.g. 'to') you should recheck that you are not trying to translate a partial sentence.
|
||||
- If you have multiple strings, that are almost identical, except some part (e.g. a word or two) it is still better to translate the full sentence multiple times. It may seem like inefficient repetition, but unlike programming where you try to minimize repetition, translation is much faster if you have many, full, clear, sentences to work with, rather than fewer, but incomplete sentence fragments.
|
||||
- Don't forget curly braces when you assign an expression to JSX attributes in the render method)
|
||||
- Don't forget curly braces when you assign an expression to JSX attributes in the render method.
|
||||
|
|
|
|||
12
package.json
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "element-web",
|
||||
"version": "1.12.6",
|
||||
"version": "1.12.7",
|
||||
"description": "Element: the future of secure communication",
|
||||
"author": "New Vector Ltd.",
|
||||
"repository": {
|
||||
|
|
@ -92,8 +92,8 @@
|
|||
"@matrix-org/spec": "^1.7.0",
|
||||
"@sentry/browser": "^10.0.0",
|
||||
"@types/png-chunks-extract": "^1.0.2",
|
||||
"@vector-im/compound-design-tokens": "6.4.1",
|
||||
"@vector-im/compound-web": "^8.3.1",
|
||||
"@vector-im/compound-design-tokens": "6.4.3",
|
||||
"@vector-im/compound-web": "^8.3.4",
|
||||
"@vector-im/matrix-wysiwyg": "2.40.0",
|
||||
"@zxcvbn-ts/core": "^3.0.4",
|
||||
"@zxcvbn-ts/language-common": "^3.0.4",
|
||||
|
|
@ -130,14 +130,14 @@
|
|||
"maplibre-gl": "^5.0.0",
|
||||
"matrix-encrypt-attachment": "^1.0.3",
|
||||
"matrix-js-sdk": "github:matrix-org/matrix-js-sdk#develop",
|
||||
"matrix-widget-api": "^1.14.0",
|
||||
"matrix-widget-api": "^1.15.0",
|
||||
"memoize-one": "^6.0.0",
|
||||
"mime": "^4.0.4",
|
||||
"oidc-client-ts": "^3.0.1",
|
||||
"opus-recorder": "^8.0.3",
|
||||
"pako": "^2.0.3",
|
||||
"png-chunks-extract": "^1.0.0",
|
||||
"posthog-js": "1.297.2",
|
||||
"posthog-js": "1.302.2",
|
||||
"qrcode": "1.5.4",
|
||||
"re-resizable": "6.11.2",
|
||||
"react": "^19.0.0",
|
||||
|
|
@ -281,7 +281,7 @@
|
|||
"postcss-preset-env": "^10.0.0",
|
||||
"postcss-scss": "^4.0.4",
|
||||
"postcss-simple-vars": "^7.0.1",
|
||||
"prettier": "3.6.2",
|
||||
"prettier": "3.7.4",
|
||||
"process": "^0.11.10",
|
||||
"raw-loader": "^4.0.2",
|
||||
"rimraf": "^6.0.0",
|
||||
|
|
|
|||
|
|
@ -43,14 +43,14 @@
|
|||
"lint:types": "tsc --noEmit --jsx react",
|
||||
"test:storybook": "test-storybook --url http://localhost:6007/",
|
||||
"test:storybook:ci": "concurrently -k -s first -n \"SB,TEST\" \"yarn storybook --no-open\" \"wait-on tcp:6007 && yarn test-storybook --url http://localhost:6007/ --ci --maxWorkers=2\"",
|
||||
"test:storybook:update": "playwright-screenshots --entrypoint yarn --with-node-modules && playwright-screenshots --entrypoint /work/node_modules/.bin/test-storybook --with-node-modules --url http://host.docker.internal:6007/ --updateSnapshot"
|
||||
"test:storybook:update": "playwright-screenshots --entrypoint /work/scripts/storybook-screenshot-update.sh --with-node-modules"
|
||||
},
|
||||
"resolutions": {
|
||||
"playwright": "1.57.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@element-hq/element-web-module-api": "^1.8.0",
|
||||
"@vector-im/compound-design-tokens": "^6.3.0",
|
||||
"@vector-im/compound-design-tokens": "^6.4.3",
|
||||
"classnames": "^2.5.1",
|
||||
"counterpart": "^0.18.6",
|
||||
"lodash": "^4.17.21",
|
||||
|
|
@ -93,6 +93,6 @@
|
|||
},
|
||||
"packageManager": "yarn@1.22.22+sha512.a6b2f7906b721bba3d67d4aff083df04dad64c399707841b7acf00f6b133b7ac24255f2652fa22ae3534329dc6180534e98d17432037ff6fd140556e2bb3137e",
|
||||
"peerDependencies": {
|
||||
"@vector-im/compound-web": "^8.2.5"
|
||||
"@vector-im/compound-web": "^8.3.4"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 18 KiB |
|
After Width: | Height: | Size: 57 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 8.4 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 7.6 KiB |
32
packages/shared-components/scripts/storybook-screenshot-update.sh
Executable file
|
|
@ -0,0 +1,32 @@
|
|||
#!/bin/bash
|
||||
#
|
||||
# Update storybook screenshots
|
||||
#
|
||||
# This script should be used as the entrypoint parameter of the `playwright-screenshots` script. It
|
||||
# installs the yarn dependencies, and then runs `test-storybook` to update the storybook screenshots.
|
||||
#
|
||||
# It expects to find a storybook instance running at :6007 on the host machine. It also requires that
|
||||
# `playwright-screenshots` is given the `--with-node-modules` parameter.
|
||||
#
|
||||
# Example:
|
||||
#
|
||||
# test-storybook --url http://localhost:6007/
|
||||
# playwright-screenshots --entrypoint /work/scripts/storybook-screenshot-update.sh --with-node-modules
|
||||
#
|
||||
#
|
||||
# Note: even though this script is small, it is important because the alternative is running
|
||||
# `playwright-screenshots` twice in quick succession (once to do `yarn install`, a second to do the
|
||||
# actual updates): and that fails, because running `playwright-screenshots` without actually starting
|
||||
# Testcontainers leaves a ryuk container hanging around for up to 60s, which will block the second
|
||||
# invocation.
|
||||
|
||||
set -e
|
||||
|
||||
# First install dependencies. We have to do this within the playwright container rather than the host,
|
||||
# because we have which must be built for the right architecture (and some environments use a VM
|
||||
# to run docker containers, meaning that things inside a container use a different architecture than
|
||||
# those on the host).
|
||||
yarn
|
||||
|
||||
# Now run the screenshot update
|
||||
/work/node_modules/.bin/test-storybook --url http://host.docker.internal:6007/ --updateSnapshot
|
||||
|
|
@ -27,8 +27,6 @@
|
|||
padding: var(--cpd-space-4x);
|
||||
|
||||
border-top: 1px solid var(--cpd-color-gray-400);
|
||||
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.banner[data-type="success"] {
|
||||
|
|
@ -90,4 +88,6 @@
|
|||
flex-direction: row;
|
||||
gap: var(--cpd-space-1x);
|
||||
align-self: center;
|
||||
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ import { type Meta, type StoryObj } from "@storybook/react-vite";
|
|||
import { Button } from "@vector-im/compound-web";
|
||||
|
||||
import { Banner } from "./Banner";
|
||||
import { _t } from "../../utils/i18n";
|
||||
|
||||
const meta = {
|
||||
title: "room/Banner",
|
||||
|
|
@ -46,17 +45,14 @@ export const WithAction: Story = {
|
|||
args: {
|
||||
children: (
|
||||
<p>
|
||||
{_t(
|
||||
"encryption|pinned_identity_changed",
|
||||
{ displayName: "Alice", userId: "@alice:example.org" },
|
||||
{
|
||||
a: (sub) => <a href="https://example.org">{sub}</a>,
|
||||
b: (sub) => <b>{sub}</b>,
|
||||
},
|
||||
)}
|
||||
Alice's (<b>@alice:example.com</b>) identity was reset. <a href="https://example.org">Learn more</a>
|
||||
</p>
|
||||
),
|
||||
actions: <Button kind="primary">{_t("encryption|withdraw_verification_action")}</Button>,
|
||||
actions: (
|
||||
<Button kind="primary" size="sm">
|
||||
Withdraw verification
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
||||
|
|
@ -71,3 +67,19 @@ export const WithoutClose: Story = {
|
|||
onClose: undefined,
|
||||
},
|
||||
};
|
||||
|
||||
export const WithLoadsOfContent: Story = {
|
||||
args: {
|
||||
type: "info",
|
||||
children: (
|
||||
<p>
|
||||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed quis massa facilisis, venenatis risus
|
||||
consectetur, sagittis libero. Aenean et scelerisque justo. Nunc luctus, mi sed facilisis suscipit, magna
|
||||
ante pharetra sem, eu rutrum purus quam quis arcu. Sed eleifend arcu vitae magna sodales, sit amet
|
||||
fermentum urna dictum. Mauris vel velit pulvinar enim mollis tincidunt. Vivamus egestas rhoncus
|
||||
sagittis. Curabitur auctor vehicula massa, et cursus lacus laoreet a. Maecenas et sollicitudin lectus,
|
||||
in ligula.
|
||||
</p>
|
||||
),
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -79,7 +79,7 @@ export function Banner({
|
|||
return (
|
||||
<div {...props} className={classes} data-type={type}>
|
||||
<div className={styles.icon}>{avatar ?? icon}</div>
|
||||
<span className={styles.content}>{children}</span>
|
||||
<div className={styles.content}>{children}</div>
|
||||
<div className={styles.actions}>
|
||||
{actions}
|
||||
{onClose && (
|
||||
|
|
|
|||
|
|
@ -26,27 +26,36 @@ exports[`AvatarWithDetails renders a banner with an action 1`] = `
|
|||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
<p>
|
||||
encryption|pinned_identity_changed
|
||||
Alice's (
|
||||
<b>
|
||||
@alice:example.com
|
||||
</b>
|
||||
) identity was reset.
|
||||
<a
|
||||
href="https://example.org"
|
||||
>
|
||||
Learn more
|
||||
</a>
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="actions"
|
||||
>
|
||||
<button
|
||||
class="_button_187yx_8"
|
||||
class="_button_13vu4_8"
|
||||
data-kind="primary"
|
||||
data-size="lg"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
encryption|withdraw_verification_action
|
||||
Withdraw verification
|
||||
</button>
|
||||
<button
|
||||
class="_button_187yx_8"
|
||||
class="_button_13vu4_8"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
|
|
@ -72,18 +81,18 @@ exports[`AvatarWithDetails renders a banner with an avatar iamge 1`] = `
|
|||
src="https://picsum.photos/32/32"
|
||||
/>
|
||||
</div>
|
||||
<span
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
<p>
|
||||
Hello! This is a status banner.
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="actions"
|
||||
>
|
||||
<button
|
||||
class="_button_187yx_8"
|
||||
class="_button_13vu4_8"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
|
|
@ -118,18 +127,18 @@ exports[`AvatarWithDetails renders a critical banner 1`] = `
|
|||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
<p>
|
||||
Hello! This is a status banner.
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="actions"
|
||||
>
|
||||
<button
|
||||
class="_button_187yx_8"
|
||||
class="_button_13vu4_8"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
|
|
@ -168,18 +177,18 @@ exports[`AvatarWithDetails renders a default banner 1`] = `
|
|||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
<p>
|
||||
Hello! This is a status banner.
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="actions"
|
||||
>
|
||||
<button
|
||||
class="_button_187yx_8"
|
||||
class="_button_13vu4_8"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
|
|
@ -219,18 +228,18 @@ exports[`AvatarWithDetails renders a info banner 1`] = `
|
|||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
<p>
|
||||
Hello! This is a status banner.
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="actions"
|
||||
>
|
||||
<button
|
||||
class="_button_187yx_8"
|
||||
class="_button_13vu4_8"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
|
|
@ -265,18 +274,18 @@ exports[`AvatarWithDetails renders a success banner 1`] = `
|
|||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
<p>
|
||||
Hello! This is a status banner.
|
||||
</p>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="actions"
|
||||
>
|
||||
<button
|
||||
class="_button_187yx_8"
|
||||
class="_button_13vu4_8"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
|
|
|
|||
|
|
@ -27,11 +27,11 @@ exports[`HistoryVisibleBannerView renders a history visible banner 1`] = `
|
|||
/>
|
||||
</svg>
|
||||
</div>
|
||||
<span
|
||||
<div
|
||||
class="content"
|
||||
>
|
||||
<span>
|
||||
Messages you send will be shared with new members invited to this room.
|
||||
This room has been configured so that new members can read history.
|
||||
<a
|
||||
class="_link_1v5rz_8"
|
||||
data-kind="primary"
|
||||
|
|
@ -40,15 +40,15 @@ exports[`HistoryVisibleBannerView renders a history visible banner 1`] = `
|
|||
rel="noreferrer noopener"
|
||||
target="_blank"
|
||||
>
|
||||
Learn more
|
||||
Learn More
|
||||
</a>
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="actions"
|
||||
>
|
||||
<button
|
||||
class="_button_187yx_8"
|
||||
class="_button_13vu4_8"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ export * from "./pill-input/Pill";
|
|||
export * from "./pill-input/PillInput";
|
||||
export * from "./rich-list/RichItem";
|
||||
export * from "./rich-list/RichList";
|
||||
export * from "./room-list/RoomListSearchView";
|
||||
export * from "./utils/Box";
|
||||
export * from "./utils/Flex";
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
.view {
|
||||
/* From figma, this should be aligned with the room header */
|
||||
min-height: 64px;
|
||||
box-sizing: border-box;
|
||||
border-bottom: var(--cpd-border-width-1) solid var(--cpd-color-bg-subtle-primary);
|
||||
padding: 0 var(--cpd-space-3x);
|
||||
}
|
||||
|
||||
.search {
|
||||
/* The search button should take all the remaining space */
|
||||
flex: 1;
|
||||
/* !important is needed to override compound button in EW */
|
||||
font: var(--cpd-font-body-md-regular) !important;
|
||||
color: var(--cpd-color-text-secondary) !important;
|
||||
min-width: 0;
|
||||
|
||||
svg {
|
||||
fill: var(--cpd-color-icon-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
.search_container {
|
||||
flex: 1;
|
||||
|
||||
/* Shrink and truncate the search text */
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
||||
kbd {
|
||||
font-family: inherit;
|
||||
}
|
||||
}
|
||||
|
||||
.search_text {
|
||||
min-width: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
text-align: start;
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type JSX } from "react";
|
||||
import { fn } from "storybook/test";
|
||||
|
||||
import type { Meta, StoryFn } from "@storybook/react-vite";
|
||||
import {
|
||||
RoomListSearchView,
|
||||
type RoomListSearchViewActions,
|
||||
type RoomListSearchViewSnapshot,
|
||||
} from "./RoomListSearchView";
|
||||
import { useMockedViewModel } from "../../useMockedViewModel";
|
||||
|
||||
type RoomListSearchProps = RoomListSearchViewSnapshot & RoomListSearchViewActions;
|
||||
|
||||
const RoomListSearchViewWrapper = ({
|
||||
onSearchClick,
|
||||
onDialPadClick,
|
||||
onExploreClick,
|
||||
...rest
|
||||
}: RoomListSearchProps): JSX.Element => {
|
||||
const vm = useMockedViewModel(rest, {
|
||||
onSearchClick,
|
||||
onDialPadClick,
|
||||
onExploreClick,
|
||||
});
|
||||
return <RoomListSearchView vm={vm} />;
|
||||
};
|
||||
|
||||
export default {
|
||||
title: "Room List/RoomListSearchView",
|
||||
component: RoomListSearchViewWrapper,
|
||||
tags: ["autodocs"],
|
||||
args: {
|
||||
displayExploreButton: true,
|
||||
displayDialButton: false,
|
||||
searchShortcut: "⌘ K",
|
||||
onSearchClick: fn(),
|
||||
onDialPadClick: fn(),
|
||||
onExploreClick: fn(),
|
||||
},
|
||||
parameters: {
|
||||
design: {
|
||||
type: "figma",
|
||||
url: "https://www.figma.com/design/vlmt46QDdE4dgXDiyBJXqp/ER-33-Left-Panel-2025?node-id=98-1979&t=vafb4zoYMNLRuAbh-4",
|
||||
},
|
||||
},
|
||||
} as Meta<typeof RoomListSearchViewWrapper>;
|
||||
|
||||
const Template: StoryFn<typeof RoomListSearchViewWrapper> = (args) => <RoomListSearchViewWrapper {...args} />;
|
||||
|
||||
export const Default = Template.bind({});
|
||||
|
||||
export const WithDialPad = Template.bind({});
|
||||
WithDialPad.args = {
|
||||
displayDialButton: true,
|
||||
};
|
||||
|
||||
export const WithoutExplore = Template.bind({});
|
||||
WithoutExplore.args = {
|
||||
displayExploreButton: false,
|
||||
};
|
||||
|
||||
export const AllButtons = Template.bind({});
|
||||
AllButtons.args = {
|
||||
displayExploreButton: true,
|
||||
displayDialButton: true,
|
||||
searchShortcut: "⌘ K",
|
||||
};
|
||||
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { render, screen } from "jest-matrix-react";
|
||||
import { composeStories } from "@storybook/react-vite";
|
||||
import React from "react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
|
||||
import * as stories from "./RoomListSearchView.stories";
|
||||
import {
|
||||
RoomListSearchView,
|
||||
type RoomListSearchViewActions,
|
||||
type RoomListSearchViewSnapshot,
|
||||
} from "./RoomListSearchView";
|
||||
import { MockViewModel } from "../../viewmodel/MockViewModel";
|
||||
|
||||
const { Default, WithDialPad, WithoutExplore, AllButtons } = composeStories(stories);
|
||||
|
||||
describe("RoomListSearchView", () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe("Storybook snapshots", () => {
|
||||
it("renders the default state", () => {
|
||||
const { container } = render(<Default />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders with dial pad button", () => {
|
||||
const { container } = render(<WithDialPad />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders without explore button", () => {
|
||||
const { container } = render(<WithoutExplore />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("renders with all buttons visible", () => {
|
||||
const { container } = render(<AllButtons />);
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe("User interactions", () => {
|
||||
const onSearchClick = jest.fn();
|
||||
const onDialPadClick = jest.fn();
|
||||
const onExploreClick = jest.fn();
|
||||
|
||||
class TestViewModel extends MockViewModel<RoomListSearchViewSnapshot> implements RoomListSearchViewActions {
|
||||
public onSearchClick = onSearchClick;
|
||||
public onDialPadClick = onDialPadClick;
|
||||
public onExploreClick = onExploreClick;
|
||||
}
|
||||
|
||||
it("should call onSearchClick when search button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const vm = new TestViewModel({
|
||||
displayExploreButton: false,
|
||||
displayDialButton: false,
|
||||
searchShortcut: "⌘ K",
|
||||
});
|
||||
|
||||
render(<RoomListSearchView vm={vm} />);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Search ⌘ K" }));
|
||||
expect(onSearchClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should call onDialPadClick when dial pad button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const vm = new TestViewModel({
|
||||
displayExploreButton: false,
|
||||
displayDialButton: true,
|
||||
searchShortcut: "⌘ K",
|
||||
});
|
||||
|
||||
render(<RoomListSearchView vm={vm} />);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Open dial pad" }));
|
||||
expect(onDialPadClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("should call onExploreClick when explore button is clicked", async () => {
|
||||
const user = userEvent.setup();
|
||||
const vm = new TestViewModel({
|
||||
displayExploreButton: true,
|
||||
displayDialButton: false,
|
||||
searchShortcut: "⌘ K",
|
||||
});
|
||||
|
||||
render(<RoomListSearchView vm={vm} />);
|
||||
|
||||
await user.click(screen.getByRole("button", { name: "Explore rooms" }));
|
||||
expect(onExploreClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,119 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import React, { type JSX, type MouseEventHandler } from "react";
|
||||
import { Button } from "@vector-im/compound-web";
|
||||
import ExploreIcon from "@vector-im/compound-design-tokens/assets/web/icons/explore";
|
||||
import SearchIcon from "@vector-im/compound-design-tokens/assets/web/icons/search";
|
||||
import DialPadIcon from "@vector-im/compound-design-tokens/assets/web/icons/dial-pad";
|
||||
|
||||
import styles from "./RoomListSearchView.module.css";
|
||||
import { type ViewModel } from "../../viewmodel/ViewModel";
|
||||
import { useViewModel } from "../../useViewModel";
|
||||
import { Flex } from "../../utils/Flex";
|
||||
import { useI18n } from "../../utils/i18nContext";
|
||||
|
||||
export interface RoomListSearchViewSnapshot {
|
||||
/**
|
||||
* Whether to display the explore button.
|
||||
*/
|
||||
displayExploreButton: boolean;
|
||||
/**
|
||||
* Whether to display the dial pad button.
|
||||
*/
|
||||
displayDialButton: boolean;
|
||||
/**
|
||||
* The keyboard shortcut text to display for the search action.
|
||||
* For example: "⌘ K" on macOS or "Ctrl K" on other platforms.
|
||||
*/
|
||||
searchShortcut: string;
|
||||
}
|
||||
|
||||
export interface RoomListSearchViewActions {
|
||||
/**
|
||||
* Handles the click event on the search button.
|
||||
*/
|
||||
onSearchClick: MouseEventHandler<HTMLButtonElement>;
|
||||
/**
|
||||
* Handles the click event on the dial pad button.
|
||||
*/
|
||||
onDialPadClick: MouseEventHandler<HTMLButtonElement>;
|
||||
/**
|
||||
* Handles the click event on the explore button.
|
||||
*/
|
||||
onExploreClick: MouseEventHandler<HTMLButtonElement>;
|
||||
}
|
||||
|
||||
/**
|
||||
* The view model for the room list search component.
|
||||
*/
|
||||
export type RoomListSearchViewModel = ViewModel<RoomListSearchViewSnapshot> & RoomListSearchViewActions;
|
||||
|
||||
interface RoomListSearchViewProps {
|
||||
/**
|
||||
* The view model for the room list search component.
|
||||
*/
|
||||
vm: RoomListSearchViewModel;
|
||||
}
|
||||
|
||||
/**
|
||||
* A search component to be displayed at the top of the room list.
|
||||
* The component provides search functionality, optional dial pad access, and optional room exploration.
|
||||
*
|
||||
* @example
|
||||
* ```tsx
|
||||
* <RoomListSearchView vm={roomListSearchViewModel} />
|
||||
* ```
|
||||
*/
|
||||
export function RoomListSearchView({ vm }: Readonly<RoomListSearchViewProps>): JSX.Element {
|
||||
const { translate: _t } = useI18n();
|
||||
const { displayExploreButton, displayDialButton, searchShortcut } = useViewModel(vm);
|
||||
|
||||
return (
|
||||
<Flex
|
||||
data-testid="room-list-search"
|
||||
className={styles.view}
|
||||
role="search"
|
||||
gap="var(--cpd-space-2x)"
|
||||
align="center"
|
||||
>
|
||||
<Button
|
||||
id="room-list-search-button"
|
||||
className={styles.search}
|
||||
kind="secondary"
|
||||
size="sm"
|
||||
Icon={SearchIcon}
|
||||
onClick={vm.onSearchClick}
|
||||
>
|
||||
<Flex className={styles["search_container"]} as="span" justify="space-between">
|
||||
<span className={styles["search_text"]}>{_t("action|search")}</span>
|
||||
<kbd>{searchShortcut}</kbd>
|
||||
</Flex>
|
||||
</Button>
|
||||
{displayDialButton && (
|
||||
<Button
|
||||
kind="secondary"
|
||||
size="sm"
|
||||
Icon={DialPadIcon}
|
||||
iconOnly={true}
|
||||
aria-label={_t("left_panel|open_dial_pad")}
|
||||
onClick={vm.onDialPadClick}
|
||||
/>
|
||||
)}
|
||||
{displayExploreButton && (
|
||||
<Button
|
||||
kind="secondary"
|
||||
size="sm"
|
||||
Icon={ExploreIcon}
|
||||
iconOnly={true}
|
||||
aria-label={_t("action|explore_rooms")}
|
||||
onClick={vm.onExploreClick}
|
||||
/>
|
||||
)}
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
|
@ -0,0 +1,290 @@
|
|||
// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing
|
||||
|
||||
exports[`RoomListSearchView Storybook snapshots renders the default state 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="flex view"
|
||||
data-testid="room-list-search"
|
||||
role="search"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
class="_button_13vu4_8 search _has-icon_13vu4_60"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
id="room-list-search-button"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M15.05 16.463a7.5 7.5 0 1 1 1.414-1.414l3.243 3.244a1 1 0 0 1-1.414 1.414zM16 10.5a5.5 5.5 0 1 0-11 0 5.5 5.5 0 0 0 11 0"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="flex search_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: space-between; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
class="search_text"
|
||||
>
|
||||
Search
|
||||
</span>
|
||||
<kbd>
|
||||
⌘ K
|
||||
</kbd>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Explore rooms"
|
||||
class="_button_13vu4_8 _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 13a.97.97 0 0 1-.713-.287A.97.97 0 0 1 11 12q0-.424.287-.713A.97.97 0 0 1 12 11q.424 0 .713.287.287.288.287.713 0 .424-.287.713A.97.97 0 0 1 12 13m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22m0-2q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4 6.325 6.325 4 12t2.325 5.675T12 20m0 0q-3.35 0-5.675-2.325T4 12t2.325-5.675T12 4t5.675 2.325T20 12t-2.325 5.675T12 20m1.675-5.85q.15-.075.275-.2t.2-.275l2.925-6.25q.125-.25-.062-.437-.188-.188-.438-.063l-6.25 2.925q-.15.075-.275.2t-.2.275l-2.925 6.25q-.125.25.063.438.186.186.437.062z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`RoomListSearchView Storybook snapshots renders with all buttons visible 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="flex view"
|
||||
data-testid="room-list-search"
|
||||
role="search"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
class="_button_13vu4_8 search _has-icon_13vu4_60"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
id="room-list-search-button"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M15.05 16.463a7.5 7.5 0 1 1 1.414-1.414l3.243 3.244a1 1 0 0 1-1.414 1.414zM16 10.5a5.5 5.5 0 1 0-11 0 5.5 5.5 0 0 0 11 0"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="flex search_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: space-between; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
class="search_text"
|
||||
>
|
||||
Search
|
||||
</span>
|
||||
<kbd>
|
||||
⌘ K
|
||||
</kbd>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Open dial pad"
|
||||
class="_button_13vu4_8 _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 18.6c-.99 0-1.8.81-1.8 1.8s.81 1.8 1.8 1.8 1.8-.81 1.8-1.8-.81-1.8-1.8-1.8M6.6 2.4c-.99 0-1.8.81-1.8 1.8S5.61 6 6.6 6s1.8-.81 1.8-1.8-.81-1.8-1.8-1.8m0 5.4c-.99 0-1.8.81-1.8 1.8s.81 1.8 1.8 1.8 1.8-.81 1.8-1.8-.81-1.8-1.8-1.8m0 5.4c-.99 0-1.8.81-1.8 1.8s.81 1.8 1.8 1.8 1.8-.81 1.8-1.8-.81-1.8-1.8-1.8M17.4 6c.99 0 1.8-.81 1.8-1.8s-.81-1.8-1.8-1.8-1.8.81-1.8 1.8.81 1.8 1.8 1.8M12 13.2c-.99 0-1.8.81-1.8 1.8s.81 1.8 1.8 1.8 1.8-.81 1.8-1.8-.81-1.8-1.8-1.8m5.4 0c-.99 0-1.8.81-1.8 1.8s.81 1.8 1.8 1.8 1.8-.81 1.8-1.8-.81-1.8-1.8-1.8m0-5.4c-.99 0-1.8.81-1.8 1.8s.81 1.8 1.8 1.8 1.8-.81 1.8-1.8-.81-1.8-1.8-1.8m-5.4 0c-.99 0-1.8.81-1.8 1.8s.81 1.8 1.8 1.8 1.8-.81 1.8-1.8-.81-1.8-1.8-1.8m0-5.4c-.99 0-1.8.81-1.8 1.8S11.01 6 12 6s1.8-.81 1.8-1.8-.81-1.8-1.8-1.8"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Explore rooms"
|
||||
class="_button_13vu4_8 _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 13a.97.97 0 0 1-.713-.287A.97.97 0 0 1 11 12q0-.424.287-.713A.97.97 0 0 1 12 11q.424 0 .713.287.287.288.287.713 0 .424-.287.713A.97.97 0 0 1 12 13m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22m0-2q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4 6.325 6.325 4 12t2.325 5.675T12 20m0 0q-3.35 0-5.675-2.325T4 12t2.325-5.675T12 4t5.675 2.325T20 12t-2.325 5.675T12 20m1.675-5.85q.15-.075.275-.2t.2-.275l2.925-6.25q.125-.25-.062-.437-.188-.188-.438-.063l-6.25 2.925q-.15.075-.275.2t-.2.275l-2.925 6.25q-.125.25.063.438.186.186.437.062z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`RoomListSearchView Storybook snapshots renders with dial pad button 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="flex view"
|
||||
data-testid="room-list-search"
|
||||
role="search"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
class="_button_13vu4_8 search _has-icon_13vu4_60"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
id="room-list-search-button"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M15.05 16.463a7.5 7.5 0 1 1 1.414-1.414l3.243 3.244a1 1 0 0 1-1.414 1.414zM16 10.5a5.5 5.5 0 1 0-11 0 5.5 5.5 0 0 0 11 0"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="flex search_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: space-between; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
class="search_text"
|
||||
>
|
||||
Search
|
||||
</span>
|
||||
<kbd>
|
||||
⌘ K
|
||||
</kbd>
|
||||
</span>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Open dial pad"
|
||||
class="_button_13vu4_8 _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 18.6c-.99 0-1.8.81-1.8 1.8s.81 1.8 1.8 1.8 1.8-.81 1.8-1.8-.81-1.8-1.8-1.8M6.6 2.4c-.99 0-1.8.81-1.8 1.8S5.61 6 6.6 6s1.8-.81 1.8-1.8-.81-1.8-1.8-1.8m0 5.4c-.99 0-1.8.81-1.8 1.8s.81 1.8 1.8 1.8 1.8-.81 1.8-1.8-.81-1.8-1.8-1.8m0 5.4c-.99 0-1.8.81-1.8 1.8s.81 1.8 1.8 1.8 1.8-.81 1.8-1.8-.81-1.8-1.8-1.8M17.4 6c.99 0 1.8-.81 1.8-1.8s-.81-1.8-1.8-1.8-1.8.81-1.8 1.8.81 1.8 1.8 1.8M12 13.2c-.99 0-1.8.81-1.8 1.8s.81 1.8 1.8 1.8 1.8-.81 1.8-1.8-.81-1.8-1.8-1.8m5.4 0c-.99 0-1.8.81-1.8 1.8s.81 1.8 1.8 1.8 1.8-.81 1.8-1.8-.81-1.8-1.8-1.8m0-5.4c-.99 0-1.8.81-1.8 1.8s.81 1.8 1.8 1.8 1.8-.81 1.8-1.8-.81-1.8-1.8-1.8m-5.4 0c-.99 0-1.8.81-1.8 1.8s.81 1.8 1.8 1.8 1.8-.81 1.8-1.8-.81-1.8-1.8-1.8m0-5.4c-.99 0-1.8.81-1.8 1.8S11.01 6 12 6s1.8-.81 1.8-1.8-.81-1.8-1.8-1.8"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<button
|
||||
aria-label="Explore rooms"
|
||||
class="_button_13vu4_8 _has-icon_13vu4_60 _icon-only_13vu4_53"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M12 13a.97.97 0 0 1-.713-.287A.97.97 0 0 1 11 12q0-.424.287-.713A.97.97 0 0 1 12 11q.424 0 .713.287.287.288.287.713 0 .424-.287.713A.97.97 0 0 1 12 13m0 9a9.7 9.7 0 0 1-3.9-.788 10.1 10.1 0 0 1-3.175-2.137q-1.35-1.35-2.137-3.175A9.7 9.7 0 0 1 2 12q0-2.075.788-3.9a10.1 10.1 0 0 1 2.137-3.175q1.35-1.35 3.175-2.137A9.7 9.7 0 0 1 12 2q2.075 0 3.9.788a10.1 10.1 0 0 1 3.175 2.137q1.35 1.35 2.137 3.175A9.7 9.7 0 0 1 22 12a9.7 9.7 0 0 1-.788 3.9 10.1 10.1 0 0 1-2.137 3.175q-1.35 1.35-3.175 2.137A9.7 9.7 0 0 1 12 22m0-2q3.35 0 5.675-2.325T20 12t-2.325-5.675T12 4 6.325 6.325 4 12t2.325 5.675T12 20m0 0q-3.35 0-5.675-2.325T4 12t2.325-5.675T12 4t5.675 2.325T20 12t-2.325 5.675T12 20m1.675-5.85q.15-.075.275-.2t.2-.275l2.925-6.25q.125-.25-.062-.437-.188-.188-.438-.063l-6.25 2.925q-.15.075-.275.2t-.2.275l-2.925 6.25q-.125.25.063.438.186.186.437.062z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`RoomListSearchView Storybook snapshots renders without explore button 1`] = `
|
||||
<div>
|
||||
<div
|
||||
class="flex view"
|
||||
data-testid="room-list-search"
|
||||
role="search"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x); --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<button
|
||||
class="_button_13vu4_8 search _has-icon_13vu4_60"
|
||||
data-kind="secondary"
|
||||
data-size="sm"
|
||||
id="room-list-search-button"
|
||||
role="button"
|
||||
tabindex="0"
|
||||
>
|
||||
<svg
|
||||
aria-hidden="true"
|
||||
fill="currentColor"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
width="20"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
>
|
||||
<path
|
||||
d="M15.05 16.463a7.5 7.5 0 1 1 1.414-1.414l3.243 3.244a1 1 0 0 1-1.414 1.414zM16 10.5a5.5 5.5 0 1 0-11 0 5.5 5.5 0 0 0 11 0"
|
||||
/>
|
||||
</svg>
|
||||
<span
|
||||
class="flex search_container"
|
||||
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: start; --mx-flex-justify: space-between; --mx-flex-gap: 0; --mx-flex-wrap: nowrap;"
|
||||
>
|
||||
<span
|
||||
class="search_text"
|
||||
>
|
||||
Search
|
||||
</span>
|
||||
<kbd>
|
||||
⌘ K
|
||||
</kbd>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* Copyright 2025 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
export type { RoomListSearchViewModel, RoomListSearchViewSnapshot } from "./RoomListSearchView";
|
||||
export { RoomListSearchView } from "./RoomListSearchView";
|
||||
|
|
@ -313,9 +313,9 @@
|
|||
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
|
||||
|
||||
"@element-hq/element-web-module-api@^1.8.0":
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/@element-hq/element-web-module-api/-/element-web-module-api-1.8.0.tgz#95aa4ec22609cf0f4a7f24274473af0645a16f2a"
|
||||
integrity sha512-lMiDA9ubP3mZZupIMT8T3wS0riX30rYZj3pFpdP4cfZhkWZa3FJFStokAy5OnaHyENC7Px1cqkBGqilOWewY/A==
|
||||
version "1.9.0"
|
||||
resolved "https://registry.yarnpkg.com/@element-hq/element-web-module-api/-/element-web-module-api-1.9.0.tgz#2e4fcc8809418c8670d4f0576bc4a9a235bc6c50"
|
||||
integrity sha512-Ao/V9w+wysZK4bh61LlKlznF10n2ZbD6KcUI46/zUMttXbmJn3ahvbzhEpwYcD+Cjy3ag5ycxLIIGkKV/fncXg==
|
||||
|
||||
"@element-hq/element-web-playwright-common@^2.0.0":
|
||||
version "2.1.0"
|
||||
|
|
@ -358,260 +358,260 @@
|
|||
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz#80fcbe36130e58b7670511e888b8e88a259ed76c"
|
||||
integrity sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==
|
||||
|
||||
"@esbuild/aix-ppc64@0.27.1":
|
||||
version "0.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.1.tgz#116edcd62c639ed8ab551e57b38251bb28384de4"
|
||||
integrity sha512-HHB50pdsBX6k47S4u5g/CaLjqS3qwaOVE5ILsq64jyzgMhLuCuZ8rGzM9yhsAjfjkbgUPMzZEPa7DAp7yz6vuA==
|
||||
"@esbuild/aix-ppc64@0.27.2":
|
||||
version "0.27.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz#521cbd968dcf362094034947f76fa1b18d2d403c"
|
||||
integrity sha512-GZMB+a0mOMZs4MpDbj8RJp4cw+w1WV5NYD6xzgvzUJ5Ek2jerwfO2eADyI6ExDSUED+1X8aMbegahsJi+8mgpw==
|
||||
|
||||
"@esbuild/android-arm64@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz#8aa4965f8d0a7982dc21734bf6601323a66da752"
|
||||
integrity sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==
|
||||
|
||||
"@esbuild/android-arm64@0.27.1":
|
||||
version "0.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.27.1.tgz#31c00d864c80f6de1900a11de8a506dbfbb27349"
|
||||
integrity sha512-45fuKmAJpxnQWixOGCrS+ro4Uvb4Re9+UTieUY2f8AEc+t7d4AaZ6eUJ3Hva7dtrxAAWHtlEFsXFMAgNnGU9uQ==
|
||||
"@esbuild/android-arm64@0.27.2":
|
||||
version "0.27.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.27.2.tgz#61ea550962d8aa12a9b33194394e007657a6df57"
|
||||
integrity sha512-pvz8ZZ7ot/RBphf8fv60ljmaoydPU12VuXHImtAs0XhLLw+EXBi2BLe3OYSBslR4rryHvweW5gmkKFwTiFy6KA==
|
||||
|
||||
"@esbuild/android-arm@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.12.tgz#300712101f7f50f1d2627a162e6e09b109b6767a"
|
||||
integrity sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==
|
||||
|
||||
"@esbuild/android-arm@0.27.1":
|
||||
version "0.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.27.1.tgz#d2b73ab0ba894923a1d1378fd4b15cc20985f436"
|
||||
integrity sha512-kFqa6/UcaTbGm/NncN9kzVOODjhZW8e+FRdSeypWe6j33gzclHtwlANs26JrupOntlcWmB0u8+8HZo8s7thHvg==
|
||||
"@esbuild/android-arm@0.27.2":
|
||||
version "0.27.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.27.2.tgz#554887821e009dd6d853f972fde6c5143f1de142"
|
||||
integrity sha512-DVNI8jlPa7Ujbr1yjU2PfUSRtAUZPG9I1RwW4F4xFB1Imiu2on0ADiI/c3td+KmDtVKNbi+nffGDQMfcIMkwIA==
|
||||
|
||||
"@esbuild/android-x64@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.12.tgz#87dfb27161202bdc958ef48bb61b09c758faee16"
|
||||
integrity sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==
|
||||
|
||||
"@esbuild/android-x64@0.27.1":
|
||||
version "0.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.27.1.tgz#d9f74d8278191317250cfe0c15a13f410540b122"
|
||||
integrity sha512-LBEpOz0BsgMEeHgenf5aqmn/lLNTFXVfoWMUox8CtWWYK9X4jmQzWjoGoNb8lmAYml/tQ/Ysvm8q7szu7BoxRQ==
|
||||
"@esbuild/android-x64@0.27.2":
|
||||
version "0.27.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.27.2.tgz#a7ce9d0721825fc578f9292a76d9e53334480ba2"
|
||||
integrity sha512-z8Ank4Byh4TJJOh4wpz8g2vDy75zFL0TlZlkUkEwYXuPSgX8yzep596n6mT7905kA9uHZsf/o2OJZubl2l3M7A==
|
||||
|
||||
"@esbuild/darwin-arm64@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz#79197898ec1ff745d21c071e1c7cc3c802f0c1fd"
|
||||
integrity sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==
|
||||
|
||||
"@esbuild/darwin-arm64@0.27.1":
|
||||
version "0.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.1.tgz#baf6914b8c57ed9d41f9de54023aa3ff9b084680"
|
||||
integrity sha512-veg7fL8eMSCVKL7IW4pxb54QERtedFDfY/ASrumK/SbFsXnRazxY4YykN/THYqFnFwJ0aVjiUrVG2PwcdAEqQQ==
|
||||
"@esbuild/darwin-arm64@0.27.2":
|
||||
version "0.27.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.2.tgz#2cb7659bd5d109803c593cfc414450d5430c8256"
|
||||
integrity sha512-davCD2Zc80nzDVRwXTcQP/28fiJbcOwvdolL0sOiOsbwBa72kegmVU0Wrh1MYrbuCL98Omp5dVhQFWRKR2ZAlg==
|
||||
|
||||
"@esbuild/darwin-x64@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz#146400a8562133f45c4d2eadcf37ddd09718079e"
|
||||
integrity sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==
|
||||
|
||||
"@esbuild/darwin-x64@0.27.1":
|
||||
version "0.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.27.1.tgz#64e37400795f780a76c858a118ff19681a64b4e0"
|
||||
integrity sha512-+3ELd+nTzhfWb07Vol7EZ+5PTbJ/u74nC6iv4/lwIU99Ip5uuY6QoIf0Hn4m2HoV0qcnRivN3KSqc+FyCHjoVQ==
|
||||
"@esbuild/darwin-x64@0.27.2":
|
||||
version "0.27.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.27.2.tgz#e741fa6b1abb0cd0364126ba34ca17fd5e7bf509"
|
||||
integrity sha512-ZxtijOmlQCBWGwbVmwOF/UCzuGIbUkqB1faQRf5akQmxRJ1ujusWsb3CVfk/9iZKr2L5SMU5wPBi1UWbvL+VQA==
|
||||
|
||||
"@esbuild/freebsd-arm64@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz#1c5f9ba7206e158fd2b24c59fa2d2c8bb47ca0fe"
|
||||
integrity sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==
|
||||
|
||||
"@esbuild/freebsd-arm64@0.27.1":
|
||||
version "0.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.1.tgz#6572f2f235933eee906e070dfaae54488ee60acd"
|
||||
integrity sha512-/8Rfgns4XD9XOSXlzUDepG8PX+AVWHliYlUkFI3K3GB6tqbdjYqdhcb4BKRd7C0BhZSoaCxhv8kTcBrcZWP+xg==
|
||||
"@esbuild/freebsd-arm64@0.27.2":
|
||||
version "0.27.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.2.tgz#2b64e7116865ca172d4ce034114c21f3c93e397c"
|
||||
integrity sha512-lS/9CN+rgqQ9czogxlMcBMGd+l8Q3Nj1MFQwBZJyoEKI50XGxwuzznYdwcav6lpOGv5BqaZXqvBSiB/kJ5op+g==
|
||||
|
||||
"@esbuild/freebsd-x64@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz#ea631f4a36beaac4b9279fa0fcc6ca29eaeeb2b3"
|
||||
integrity sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==
|
||||
|
||||
"@esbuild/freebsd-x64@0.27.1":
|
||||
version "0.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.1.tgz#83105dba9cf6ac4f44336799446d7f75c8c3a1e1"
|
||||
integrity sha512-GITpD8dK9C+r+5yRT/UKVT36h/DQLOHdwGVwwoHidlnA168oD3uxA878XloXebK4Ul3gDBBIvEdL7go9gCUFzQ==
|
||||
"@esbuild/freebsd-x64@0.27.2":
|
||||
version "0.27.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.2.tgz#e5252551e66f499e4934efb611812f3820e990bb"
|
||||
integrity sha512-tAfqtNYb4YgPnJlEFu4c212HYjQWSO/w/h/lQaBK7RbwGIkBOuNKQI9tqWzx7Wtp7bTPaGC6MJvWI608P3wXYA==
|
||||
|
||||
"@esbuild/linux-arm64@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz#e1066bce58394f1b1141deec8557a5f0a22f5977"
|
||||
integrity sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==
|
||||
|
||||
"@esbuild/linux-arm64@0.27.1":
|
||||
version "0.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.27.1.tgz#035ff647d4498bdf16eb2d82801f73b366477dfa"
|
||||
integrity sha512-W9//kCrh/6in9rWIBdKaMtuTTzNj6jSeG/haWBADqLLa9P8O5YSRDzgD5y9QBok4AYlzS6ARHifAb75V6G670Q==
|
||||
"@esbuild/linux-arm64@0.27.2":
|
||||
version "0.27.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.27.2.tgz#dc4acf235531cd6984f5d6c3b13dbfb7ddb303cb"
|
||||
integrity sha512-hYxN8pr66NsCCiRFkHUAsxylNOcAQaxSSkHMMjcpx0si13t1LHFphxJZUiGwojB1a/Hd5OiPIqDdXONia6bhTw==
|
||||
|
||||
"@esbuild/linux-arm@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz#452cd66b20932d08bdc53a8b61c0e30baf4348b9"
|
||||
integrity sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==
|
||||
|
||||
"@esbuild/linux-arm@0.27.1":
|
||||
version "0.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.27.1.tgz#3516c74d2afbe305582dbb546d60f7978a8ece7f"
|
||||
integrity sha512-ieMID0JRZY/ZeCrsFQ3Y3NlHNCqIhTprJfDgSB3/lv5jJZ8FX3hqPyXWhe+gvS5ARMBJ242PM+VNz/ctNj//eA==
|
||||
"@esbuild/linux-arm@0.27.2":
|
||||
version "0.27.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.27.2.tgz#56a900e39240d7d5d1d273bc053daa295c92e322"
|
||||
integrity sha512-vWfq4GaIMP9AIe4yj1ZUW18RDhx6EPQKjwe7n8BbIecFtCQG4CfHGaHuh7fdfq+y3LIA2vGS/o9ZBGVxIDi9hw==
|
||||
|
||||
"@esbuild/linux-ia32@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz#b24f8acc45bcf54192c7f2f3be1b53e6551eafe0"
|
||||
integrity sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==
|
||||
|
||||
"@esbuild/linux-ia32@0.27.1":
|
||||
version "0.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.27.1.tgz#788db5db8ecd3d75dd41c42de0fe8f1fd967a4a7"
|
||||
integrity sha512-VIUV4z8GD8rtSVMfAj1aXFahsi/+tcoXXNYmXgzISL+KB381vbSTNdeZHHHIYqFyXcoEhu9n5cT+05tRv13rlw==
|
||||
"@esbuild/linux-ia32@0.27.2":
|
||||
version "0.27.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.27.2.tgz#d4a36d473360f6870efcd19d52bbfff59a2ed1cc"
|
||||
integrity sha512-MJt5BRRSScPDwG2hLelYhAAKh9imjHK5+NE/tvnRLbIqUWa+0E9N4WNMjmp/kXXPHZGqPLxggwVhz7QP8CTR8w==
|
||||
|
||||
"@esbuild/linux-loong64@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz#f9cfffa7fc8322571fbc4c8b3268caf15bd81ad0"
|
||||
integrity sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==
|
||||
|
||||
"@esbuild/linux-loong64@0.27.1":
|
||||
version "0.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.27.1.tgz#8211f08b146916a6302ec2b8f87ec0cc4b62c49e"
|
||||
integrity sha512-l4rfiiJRN7sTNI//ff65zJ9z8U+k6zcCg0LALU5iEWzY+a1mVZ8iWC1k5EsNKThZ7XCQ6YWtsZ8EWYm7r1UEsg==
|
||||
"@esbuild/linux-loong64@0.27.2":
|
||||
version "0.27.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.27.2.tgz#fcf0ab8c3eaaf45891d0195d4961cb18b579716a"
|
||||
integrity sha512-lugyF1atnAT463aO6KPshVCJK5NgRnU4yb3FUumyVz+cGvZbontBgzeGFO1nF+dPueHD367a2ZXe1NtUkAjOtg==
|
||||
|
||||
"@esbuild/linux-mips64el@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz#575a14bd74644ffab891adc7d7e60d275296f2cd"
|
||||
integrity sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==
|
||||
|
||||
"@esbuild/linux-mips64el@0.27.1":
|
||||
version "0.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.1.tgz#cc58586ea83b3f171e727a624e7883a1c3eb4c04"
|
||||
integrity sha512-U0bEuAOLvO/DWFdygTHWY8C067FXz+UbzKgxYhXC0fDieFa0kDIra1FAhsAARRJbvEyso8aAqvPdNxzWuStBnA==
|
||||
"@esbuild/linux-mips64el@0.27.2":
|
||||
version "0.27.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.2.tgz#598b67d34048bb7ee1901cb12e2a0a434c381c10"
|
||||
integrity sha512-nlP2I6ArEBewvJ2gjrrkESEZkB5mIoaTswuqNFRv/WYd+ATtUpe9Y09RnJvgvdag7he0OWgEZWhviS1OTOKixw==
|
||||
|
||||
"@esbuild/linux-ppc64@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz#75b99c70a95fbd5f7739d7692befe60601591869"
|
||||
integrity sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==
|
||||
|
||||
"@esbuild/linux-ppc64@0.27.1":
|
||||
version "0.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.1.tgz#632477bbd98175cf8e53a7c9952d17fb2d6d4115"
|
||||
integrity sha512-NzdQ/Xwu6vPSf/GkdmRNsOfIeSGnh7muundsWItmBsVpMoNPVpM61qNzAVY3pZ1glzzAxLR40UyYM23eaDDbYQ==
|
||||
"@esbuild/linux-ppc64@0.27.2":
|
||||
version "0.27.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.2.tgz#3846c5df6b2016dab9bc95dde26c40f11e43b4c0"
|
||||
integrity sha512-C92gnpey7tUQONqg1n6dKVbx3vphKtTHJaNG2Ok9lGwbZil6DrfyecMsp9CrmXGQJmZ7iiVXvvZH6Ml5hL6XdQ==
|
||||
|
||||
"@esbuild/linux-riscv64@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz#2e3259440321a44e79ddf7535c325057da875cd6"
|
||||
integrity sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==
|
||||
|
||||
"@esbuild/linux-riscv64@0.27.1":
|
||||
version "0.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.1.tgz#35435a82435a8a750edf433b83ac0d10239ac3fe"
|
||||
integrity sha512-7zlw8p3IApcsN7mFw0O1Z1PyEk6PlKMu18roImfl3iQHTnr/yAfYv6s4hXPidbDoI2Q0pW+5xeoM4eTCC0UdrQ==
|
||||
"@esbuild/linux-riscv64@0.27.2":
|
||||
version "0.27.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.2.tgz#173d4475b37c8d2c3e1707e068c174bb3f53d07d"
|
||||
integrity sha512-B5BOmojNtUyN8AXlK0QJyvjEZkWwy/FKvakkTDCziX95AowLZKR6aCDhG7LeF7uMCXEJqwa8Bejz5LTPYm8AvA==
|
||||
|
||||
"@esbuild/linux-s390x@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz#17676cabbfe5928da5b2a0d6df5d58cd08db2663"
|
||||
integrity sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==
|
||||
|
||||
"@esbuild/linux-s390x@0.27.1":
|
||||
version "0.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.27.1.tgz#172edd7086438edacd86c0e2ea25ac9dbb62aac5"
|
||||
integrity sha512-cGj5wli+G+nkVQdZo3+7FDKC25Uh4ZVwOAK6A06Hsvgr8WqBBuOy/1s+PUEd/6Je+vjfm6stX0kmib5b/O2Ykw==
|
||||
"@esbuild/linux-s390x@0.27.2":
|
||||
version "0.27.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.27.2.tgz#f7a4790105edcab8a5a31df26fbfac1aa3dacfab"
|
||||
integrity sha512-p4bm9+wsPwup5Z8f4EpfN63qNagQ47Ua2znaqGH6bqLlmJ4bx97Y9JdqxgGZ6Y8xVTixUnEkoKSHcpRlDnNr5w==
|
||||
|
||||
"@esbuild/linux-x64@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz#0583775685ca82066d04c3507f09524d3cd7a306"
|
||||
integrity sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==
|
||||
|
||||
"@esbuild/linux-x64@0.27.1":
|
||||
version "0.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.27.1.tgz#09c771de9e2d8169d5969adf298ae21581f08c7f"
|
||||
integrity sha512-z3H/HYI9MM0HTv3hQZ81f+AKb+yEoCRlUby1F80vbQ5XdzEMyY/9iNlAmhqiBKw4MJXwfgsh7ERGEOhrM1niMA==
|
||||
"@esbuild/linux-x64@0.27.2":
|
||||
version "0.27.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.27.2.tgz#2ecc1284b1904aeb41e54c9ddc7fcd349b18f650"
|
||||
integrity sha512-uwp2Tip5aPmH+NRUwTcfLb+W32WXjpFejTIOWZFw/v7/KnpCDKG66u4DLcurQpiYTiYwQ9B7KOeMJvLCu/OvbA==
|
||||
|
||||
"@esbuild/netbsd-arm64@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz#f04c4049cb2e252fe96b16fed90f70746b13f4a4"
|
||||
integrity sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==
|
||||
|
||||
"@esbuild/netbsd-arm64@0.27.1":
|
||||
version "0.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.1.tgz#475ac0ce7edf109a358b1669f67759de4bcbb7c4"
|
||||
integrity sha512-wzC24DxAvk8Em01YmVXyjl96Mr+ecTPyOuADAvjGg+fyBpGmxmcr2E5ttf7Im8D0sXZihpxzO1isus8MdjMCXQ==
|
||||
"@esbuild/netbsd-arm64@0.27.2":
|
||||
version "0.27.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.2.tgz#e2863c2cd1501845995cb11adf26f7fe4be527b0"
|
||||
integrity sha512-Kj6DiBlwXrPsCRDeRvGAUb/LNrBASrfqAIok+xB0LxK8CHqxZ037viF13ugfsIpePH93mX7xfJp97cyDuTZ3cw==
|
||||
|
||||
"@esbuild/netbsd-x64@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz#77da0d0a0d826d7c921eea3d40292548b258a076"
|
||||
integrity sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==
|
||||
|
||||
"@esbuild/netbsd-x64@0.27.1":
|
||||
version "0.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.1.tgz#3c31603d592477dc43b63df1ae100000f7fb59d7"
|
||||
integrity sha512-1YQ8ybGi2yIXswu6eNzJsrYIGFpnlzEWRl6iR5gMgmsrR0FcNoV1m9k9sc3PuP5rUBLshOZylc9nqSgymI+TYg==
|
||||
"@esbuild/netbsd-x64@0.27.2":
|
||||
version "0.27.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.2.tgz#93f7609e2885d1c0b5a1417885fba8d1fcc41272"
|
||||
integrity sha512-HwGDZ0VLVBY3Y+Nw0JexZy9o/nUAWq9MlV7cahpaXKW6TOzfVno3y3/M8Ga8u8Yr7GldLOov27xiCnqRZf0tCA==
|
||||
|
||||
"@esbuild/openbsd-arm64@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz#6296f5867aedef28a81b22ab2009c786a952dccd"
|
||||
integrity sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==
|
||||
|
||||
"@esbuild/openbsd-arm64@0.27.1":
|
||||
version "0.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.1.tgz#482067c847665b10d66431e936d4bc5fa8025abf"
|
||||
integrity sha512-5Z+DzLCrq5wmU7RDaMDe2DVXMRm2tTDvX2KU14JJVBN2CT/qov7XVix85QoJqHltpvAOZUAc3ndU56HSMWrv8g==
|
||||
"@esbuild/openbsd-arm64@0.27.2":
|
||||
version "0.27.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.2.tgz#a1985604a203cdc325fd47542e106fafd698f02e"
|
||||
integrity sha512-DNIHH2BPQ5551A7oSHD0CKbwIA/Ox7+78/AWkbS5QoRzaqlev2uFayfSxq68EkonB+IKjiuxBFoV8ESJy8bOHA==
|
||||
|
||||
"@esbuild/openbsd-x64@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz#f8d23303360e27b16cf065b23bbff43c14142679"
|
||||
integrity sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==
|
||||
|
||||
"@esbuild/openbsd-x64@0.27.1":
|
||||
version "0.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.1.tgz#687a188c2b184e5b671c5f74a6cd6247c0718c52"
|
||||
integrity sha512-Q73ENzIdPF5jap4wqLtsfh8YbYSZ8Q0wnxplOlZUOyZy7B4ZKW8DXGWgTCZmF8VWD7Tciwv5F4NsRf6vYlZtqg==
|
||||
"@esbuild/openbsd-x64@0.27.2":
|
||||
version "0.27.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.2.tgz#8209e46c42f1ffbe6e4ef77a32e1f47d404ad42a"
|
||||
integrity sha512-/it7w9Nb7+0KFIzjalNJVR5bOzA9Vay+yIPLVHfIQYG/j+j9VTH84aNB8ExGKPU4AzfaEvN9/V4HV+F+vo8OEg==
|
||||
|
||||
"@esbuild/openharmony-arm64@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz#49e0b768744a3924be0d7fd97dd6ce9b2923d88d"
|
||||
integrity sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==
|
||||
|
||||
"@esbuild/openharmony-arm64@0.27.1":
|
||||
version "0.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.1.tgz#9929ee7fa8c1db2f33ef4d86198018dac9c1744f"
|
||||
integrity sha512-ajbHrGM/XiK+sXM0JzEbJAen+0E+JMQZ2l4RR4VFwvV9JEERx+oxtgkpoKv1SevhjavK2z2ReHk32pjzktWbGg==
|
||||
"@esbuild/openharmony-arm64@0.27.2":
|
||||
version "0.27.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.2.tgz#8fade4441893d9cc44cbd7dcf3776f508ab6fb2f"
|
||||
integrity sha512-LRBbCmiU51IXfeXk59csuX/aSaToeG7w48nMwA6049Y4J4+VbWALAuXcs+qcD04rHDuSCSRKdmY63sruDS5qag==
|
||||
|
||||
"@esbuild/sunos-x64@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz#a6ed7d6778d67e528c81fb165b23f4911b9b13d6"
|
||||
integrity sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==
|
||||
|
||||
"@esbuild/sunos-x64@0.27.1":
|
||||
version "0.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.27.1.tgz#94071a146f313e7394c6424af07b2b564f1f994d"
|
||||
integrity sha512-IPUW+y4VIjuDVn+OMzHc5FV4GubIwPnsz6ubkvN8cuhEqH81NovB53IUlrlBkPMEPxvNnf79MGBoz8rZ2iW8HA==
|
||||
"@esbuild/sunos-x64@0.27.2":
|
||||
version "0.27.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.27.2.tgz#980d4b9703a16f0f07016632424fc6d9a789dfc2"
|
||||
integrity sha512-kMtx1yqJHTmqaqHPAzKCAkDaKsffmXkPHThSfRwZGyuqyIeBvf08KSsYXl+abf5HDAPMJIPnbBfXvP2ZC2TfHg==
|
||||
|
||||
"@esbuild/win32-arm64@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz#9ac14c378e1b653af17d08e7d3ce34caef587323"
|
||||
integrity sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==
|
||||
|
||||
"@esbuild/win32-arm64@0.27.1":
|
||||
version "0.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.27.1.tgz#869fde72a3576fdf48824085d05493fceebe395d"
|
||||
integrity sha512-RIVRWiljWA6CdVu8zkWcRmGP7iRRIIwvhDKem8UMBjPql2TXM5PkDVvvrzMtj1V+WFPB4K7zkIGM7VzRtFkjdg==
|
||||
"@esbuild/win32-arm64@0.27.2":
|
||||
version "0.27.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.27.2.tgz#1c09a3633c949ead3d808ba37276883e71f6111a"
|
||||
integrity sha512-Yaf78O/B3Kkh+nKABUF++bvJv5Ijoy9AN1ww904rOXZFLWVc5OLOfL56W+C8F9xn5JQZa3UX6m+IktJnIb1Jjg==
|
||||
|
||||
"@esbuild/win32-ia32@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz#918942dcbbb35cc14fca39afb91b5e6a3d127267"
|
||||
integrity sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==
|
||||
|
||||
"@esbuild/win32-ia32@0.27.1":
|
||||
version "0.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.27.1.tgz#31d7585893ed7b54483d0b8d87a4bfeba0ecfff5"
|
||||
integrity sha512-2BR5M8CPbptC1AK5JbJT1fWrHLvejwZidKx3UMSF0ecHMa+smhi16drIrCEggkgviBwLYd5nwrFLSl5Kho96RQ==
|
||||
"@esbuild/win32-ia32@0.27.2":
|
||||
version "0.27.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.27.2.tgz#1b1e3a63ad4bef82200fef4e369e0fff7009eee5"
|
||||
integrity sha512-Iuws0kxo4yusk7sw70Xa2E2imZU5HoixzxfGCdxwBdhiDgt9vX9VUCBhqcwY7/uh//78A1hMkkROMJq9l27oLQ==
|
||||
|
||||
"@esbuild/win32-x64@0.25.12":
|
||||
version "0.25.12"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz#9bdad8176be7811ad148d1f8772359041f46c6c5"
|
||||
integrity sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==
|
||||
|
||||
"@esbuild/win32-x64@0.27.1":
|
||||
version "0.27.1"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.27.1.tgz#5efe5a112938b1180e98c76685ff9185cfa4f16e"
|
||||
integrity sha512-d5X6RMYv6taIymSk8JBP+nxv8DQAMY6A51GPgusqLdK9wBz5wWIXy1KjTck6HnjE9hqJzJRdk+1p/t5soSbCtw==
|
||||
"@esbuild/win32-x64@0.27.2":
|
||||
version "0.27.2"
|
||||
resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.27.2.tgz#9e585ab6086bef994c6e8a5b3a0481219ada862b"
|
||||
integrity sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ==
|
||||
|
||||
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.7.0":
|
||||
version "4.9.0"
|
||||
|
|
@ -2031,10 +2031,10 @@
|
|||
resolved "https://registry.yarnpkg.com/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz#538b1e103bf8d9864e7b85cc96fa8d6fb6c40777"
|
||||
integrity sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==
|
||||
|
||||
"@vector-im/compound-design-tokens@^6.3.0":
|
||||
version "6.4.0"
|
||||
resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-6.4.0.tgz#2e51f39f79ebda985a2f6cf80d567b9307aff03a"
|
||||
integrity sha512-93nYQZMgUt6apjCwwnMhMxN8VYQXN3GYOnwovwJjavImwsCGwI/e853BV/DstrWumYh6k5pZsP9e6AF+nz3SIQ==
|
||||
"@vector-im/compound-design-tokens@^6.4.3":
|
||||
version "6.4.3"
|
||||
resolved "https://registry.yarnpkg.com/@vector-im/compound-design-tokens/-/compound-design-tokens-6.4.3.tgz#f8a98308650e841ffba6ca0820055e2bfe2b8af1"
|
||||
integrity sha512-Zyqag2QCAfpRipLHSX/LEXj6OGFgBpilMQvNdr3a0iXeGASfkM3HZGid077kMRp9lvyHiQfPIs1BdllENwDSNQ==
|
||||
|
||||
"@vitest/expect@3.2.4":
|
||||
version "3.2.4"
|
||||
|
|
@ -2729,6 +2729,13 @@ builtin-status-codes@^3.0.0:
|
|||
resolved "https://registry.yarnpkg.com/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz#85982878e21b98e1c66425e03d0174788f569ee8"
|
||||
integrity sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==
|
||||
|
||||
bundle-name@^4.1.0:
|
||||
version "4.1.0"
|
||||
resolved "https://registry.yarnpkg.com/bundle-name/-/bundle-name-4.1.0.tgz#f3b96b34160d6431a19d7688135af7cfb8797889"
|
||||
integrity sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==
|
||||
dependencies:
|
||||
run-applescript "^7.0.0"
|
||||
|
||||
byline@^5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/byline/-/byline-5.0.0.tgz#741c5216468eadc457b03410118ad77de8c1ddb1"
|
||||
|
|
@ -3169,6 +3176,19 @@ deepmerge@^4.3.1:
|
|||
resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a"
|
||||
integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==
|
||||
|
||||
default-browser-id@^5.0.0:
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/default-browser-id/-/default-browser-id-5.0.1.tgz#f7a7ccb8f5104bf8e0f71ba3b1ccfa5eafdb21e8"
|
||||
integrity sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q==
|
||||
|
||||
default-browser@^5.2.1:
|
||||
version "5.4.0"
|
||||
resolved "https://registry.yarnpkg.com/default-browser/-/default-browser-5.4.0.tgz#b55cf335bb0b465dd7c961a02cd24246aa434287"
|
||||
integrity sha512-XDuvSq38Hr1MdN47EDvYtx3U0MTqpCEn+F6ft8z2vYDzMrvQhVp0ui9oQdqW3MvK3vqUETglt1tVGgjLuJ5izg==
|
||||
dependencies:
|
||||
bundle-name "^4.1.0"
|
||||
default-browser-id "^5.0.0"
|
||||
|
||||
default-require-extensions@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/default-require-extensions/-/default-require-extensions-3.0.1.tgz#bfae00feeaeada68c2ae256c62540f60b80625bd"
|
||||
|
|
@ -3185,6 +3205,11 @@ define-data-property@^1.0.1, define-data-property@^1.1.4:
|
|||
es-errors "^1.3.0"
|
||||
gopd "^1.0.1"
|
||||
|
||||
define-lazy-prop@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz#dbb19adfb746d7fc6d734a06b72f4a00d021255f"
|
||||
integrity sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==
|
||||
|
||||
define-properties@^1.1.3, define-properties@^1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c"
|
||||
|
|
@ -3437,36 +3462,36 @@ es6-error@^4.0.1:
|
|||
integrity sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==
|
||||
|
||||
"esbuild@^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0":
|
||||
version "0.27.1"
|
||||
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.27.1.tgz#56bf43e6a4b4d2004642ec7c091b78de02b0831a"
|
||||
integrity sha512-yY35KZckJJuVVPXpvjgxiCuVEJT67F6zDeVTv4rizyPrfGBUpZQsvmxnN+C371c2esD/hNMjj4tpBhuueLN7aA==
|
||||
version "0.27.2"
|
||||
resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.27.2.tgz#d83ed2154d5813a5367376bb2292a9296fc83717"
|
||||
integrity sha512-HyNQImnsOC7X9PMNaCIeAm4ISCQXs5a5YasTXVliKv4uuBo1dKrG0A+uQS8M5eXjVMnLg3WgXaKvprHlFJQffw==
|
||||
optionalDependencies:
|
||||
"@esbuild/aix-ppc64" "0.27.1"
|
||||
"@esbuild/android-arm" "0.27.1"
|
||||
"@esbuild/android-arm64" "0.27.1"
|
||||
"@esbuild/android-x64" "0.27.1"
|
||||
"@esbuild/darwin-arm64" "0.27.1"
|
||||
"@esbuild/darwin-x64" "0.27.1"
|
||||
"@esbuild/freebsd-arm64" "0.27.1"
|
||||
"@esbuild/freebsd-x64" "0.27.1"
|
||||
"@esbuild/linux-arm" "0.27.1"
|
||||
"@esbuild/linux-arm64" "0.27.1"
|
||||
"@esbuild/linux-ia32" "0.27.1"
|
||||
"@esbuild/linux-loong64" "0.27.1"
|
||||
"@esbuild/linux-mips64el" "0.27.1"
|
||||
"@esbuild/linux-ppc64" "0.27.1"
|
||||
"@esbuild/linux-riscv64" "0.27.1"
|
||||
"@esbuild/linux-s390x" "0.27.1"
|
||||
"@esbuild/linux-x64" "0.27.1"
|
||||
"@esbuild/netbsd-arm64" "0.27.1"
|
||||
"@esbuild/netbsd-x64" "0.27.1"
|
||||
"@esbuild/openbsd-arm64" "0.27.1"
|
||||
"@esbuild/openbsd-x64" "0.27.1"
|
||||
"@esbuild/openharmony-arm64" "0.27.1"
|
||||
"@esbuild/sunos-x64" "0.27.1"
|
||||
"@esbuild/win32-arm64" "0.27.1"
|
||||
"@esbuild/win32-ia32" "0.27.1"
|
||||
"@esbuild/win32-x64" "0.27.1"
|
||||
"@esbuild/aix-ppc64" "0.27.2"
|
||||
"@esbuild/android-arm" "0.27.2"
|
||||
"@esbuild/android-arm64" "0.27.2"
|
||||
"@esbuild/android-x64" "0.27.2"
|
||||
"@esbuild/darwin-arm64" "0.27.2"
|
||||
"@esbuild/darwin-x64" "0.27.2"
|
||||
"@esbuild/freebsd-arm64" "0.27.2"
|
||||
"@esbuild/freebsd-x64" "0.27.2"
|
||||
"@esbuild/linux-arm" "0.27.2"
|
||||
"@esbuild/linux-arm64" "0.27.2"
|
||||
"@esbuild/linux-ia32" "0.27.2"
|
||||
"@esbuild/linux-loong64" "0.27.2"
|
||||
"@esbuild/linux-mips64el" "0.27.2"
|
||||
"@esbuild/linux-ppc64" "0.27.2"
|
||||
"@esbuild/linux-riscv64" "0.27.2"
|
||||
"@esbuild/linux-s390x" "0.27.2"
|
||||
"@esbuild/linux-x64" "0.27.2"
|
||||
"@esbuild/netbsd-arm64" "0.27.2"
|
||||
"@esbuild/netbsd-x64" "0.27.2"
|
||||
"@esbuild/openbsd-arm64" "0.27.2"
|
||||
"@esbuild/openbsd-x64" "0.27.2"
|
||||
"@esbuild/openharmony-arm64" "0.27.2"
|
||||
"@esbuild/sunos-x64" "0.27.2"
|
||||
"@esbuild/win32-arm64" "0.27.2"
|
||||
"@esbuild/win32-ia32" "0.27.2"
|
||||
"@esbuild/win32-x64" "0.27.2"
|
||||
|
||||
esbuild@^0.25.0:
|
||||
version "0.25.12"
|
||||
|
|
@ -3526,9 +3551,9 @@ eslint-plugin-matrix-org@^3.0.0:
|
|||
integrity sha512-xOPMeyLxOgoB2SsPhJmIc+drorZmXozSBo3X9whk/62DvbVpJcnththOCGx8ljYScADLb+baNOeN+wKZqwkldw==
|
||||
|
||||
eslint-plugin-storybook@^10.0.7:
|
||||
version "10.1.5"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-storybook/-/eslint-plugin-storybook-10.1.5.tgz#9b7a772db7cd25e6b18d2e10640424518ade21df"
|
||||
integrity sha512-aP+dJQ8/tz9a49XVvjPXeiX6IS/LclxDwfKhAW2u56s7lNltMb//GcLIRXU8MG4zfA2v0MCTb6dCu/mlxyJCNQ==
|
||||
version "10.1.10"
|
||||
resolved "https://registry.yarnpkg.com/eslint-plugin-storybook/-/eslint-plugin-storybook-10.1.10.tgz#030a55e58a5e96a513cbf873434a840abdaf6a7d"
|
||||
integrity sha512-ITr6Aq3buR/DuDATkq1BafUVJLybyo676fY+tj9Zjd1Ak+UXBAMQcQ++tiBVVHm1RqADwM3b1o6bnWHK2fPPKw==
|
||||
dependencies:
|
||||
"@typescript-eslint/utils" "^8.8.1"
|
||||
|
||||
|
|
@ -4319,6 +4344,11 @@ is-docker@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-2.2.1.tgz#33eeabe23cfe86f14bde4408a02c0cfb853acdaa"
|
||||
integrity sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==
|
||||
|
||||
is-docker@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-docker/-/is-docker-3.0.0.tgz#90093aa3106277d8a77a5910dbae71747e15a200"
|
||||
integrity sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==
|
||||
|
||||
is-extglob@^2.1.1:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2"
|
||||
|
|
@ -4352,6 +4382,13 @@ is-glob@^4.0.0, is-glob@^4.0.3:
|
|||
dependencies:
|
||||
is-extglob "^2.1.1"
|
||||
|
||||
is-inside-container@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-inside-container/-/is-inside-container-1.0.0.tgz#e81fba699662eb31dbdaf26766a61d4814717ea4"
|
||||
integrity sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==
|
||||
dependencies:
|
||||
is-docker "^3.0.0"
|
||||
|
||||
is-nan@^1.3.2:
|
||||
version "1.3.2"
|
||||
resolved "https://registry.yarnpkg.com/is-nan/-/is-nan-1.3.2.tgz#043a54adea31748b55b6cd4e09aadafa69bd9e1d"
|
||||
|
|
@ -4414,6 +4451,13 @@ is-wsl@^2.1.1:
|
|||
dependencies:
|
||||
is-docker "^2.0.0"
|
||||
|
||||
is-wsl@^3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/is-wsl/-/is-wsl-3.1.0.tgz#e1c657e39c10090afcbedec61720f6b924c3cbd2"
|
||||
integrity sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==
|
||||
dependencies:
|
||||
is-inside-container "^1.0.0"
|
||||
|
||||
isarray@^2.0.5:
|
||||
version "2.0.5"
|
||||
resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723"
|
||||
|
|
@ -5541,6 +5585,16 @@ onetime@^5.1.2:
|
|||
dependencies:
|
||||
mimic-fn "^2.1.0"
|
||||
|
||||
open@^10.2.0:
|
||||
version "10.2.0"
|
||||
resolved "https://registry.yarnpkg.com/open/-/open-10.2.0.tgz#b9d855be007620e80b6fb05fac98141fe62db73c"
|
||||
integrity sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==
|
||||
dependencies:
|
||||
default-browser "^5.2.1"
|
||||
define-lazy-prop "^3.0.0"
|
||||
is-inside-container "^1.0.0"
|
||||
wsl-utils "^0.1.0"
|
||||
|
||||
open@^7.4.2:
|
||||
version "7.4.2"
|
||||
resolved "https://registry.yarnpkg.com/open/-/open-7.4.2.tgz#b8147e26dcf3e426316c730089fd71edd29c2321"
|
||||
|
|
@ -5855,9 +5909,9 @@ prelude-ls@^1.2.1:
|
|||
integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
|
||||
|
||||
prettier@^3.6.2:
|
||||
version "3.6.2"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.6.2.tgz#ccda02a1003ebbb2bfda6f83a074978f608b9393"
|
||||
integrity sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==
|
||||
version "3.7.4"
|
||||
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.7.4.tgz#d2f8335d4b1cec47e1c8098645411b0c9dff9c0f"
|
||||
integrity sha512-v6UNi1+3hSlVvv8fSaoUbggEM5VErKmmpGA7Pl3HF8V6uKY7rvClBOJlH6yNwQtfTueNkGVpOv/mtWL9L4bgRA==
|
||||
|
||||
pretty-format@30.2.0, pretty-format@^30.0.0:
|
||||
version "30.2.0"
|
||||
|
|
@ -5977,9 +6031,9 @@ pure-rand@^7.0.0:
|
|||
integrity sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==
|
||||
|
||||
qs@^6.12.3:
|
||||
version "6.14.0"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.0.tgz#c63fa40680d2c5c941412a0e899c89af60c0a930"
|
||||
integrity sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==
|
||||
version "6.14.1"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.14.1.tgz#a41d85b9d3902f31d27861790506294881871159"
|
||||
integrity sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==
|
||||
dependencies:
|
||||
side-channel "^1.1.0"
|
||||
|
||||
|
|
@ -6241,6 +6295,11 @@ rollup@^4.43.0:
|
|||
"@rollup/rollup-win32-x64-msvc" "4.53.3"
|
||||
fsevents "~2.3.2"
|
||||
|
||||
run-applescript@^7.0.0:
|
||||
version "7.1.0"
|
||||
resolved "https://registry.yarnpkg.com/run-applescript/-/run-applescript-7.1.0.tgz#2e9e54c4664ec3106c5b5630e249d3d6595c4911"
|
||||
integrity sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q==
|
||||
|
||||
run-parallel@^1.1.9:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee"
|
||||
|
|
@ -6506,9 +6565,9 @@ stack-utils@^2.0.6:
|
|||
escape-string-regexp "^2.0.0"
|
||||
|
||||
storybook@^10.0.7:
|
||||
version "10.1.5"
|
||||
resolved "https://registry.yarnpkg.com/storybook/-/storybook-10.1.5.tgz#bae43cfb52496bcab29916ca9936915cf9d77dfb"
|
||||
integrity sha512-q3xB1pOcmmHUH9LfQNY/BWMGxp3fc1OALJf+F5BXIxHGQUEIizz6V1AbDOngWN9oWzuA8Gdz5rOCe7yelOMWVg==
|
||||
version "10.1.10"
|
||||
resolved "https://registry.yarnpkg.com/storybook/-/storybook-10.1.10.tgz#9023e063f97cb2c38b4520e03d49336bcbf6661f"
|
||||
integrity sha512-oK0t0jEogiKKfv5Z1ao4Of99+xWw1TMUGuGRYDQS4kp2yyBsJQEgu7NI7OLYsCDI6gzt5p3RPtl1lqdeVLUi8A==
|
||||
dependencies:
|
||||
"@storybook/global" "^5.0.0"
|
||||
"@storybook/icons" "^2.0.0"
|
||||
|
|
@ -6517,6 +6576,7 @@ storybook@^10.0.7:
|
|||
"@vitest/expect" "3.2.4"
|
||||
"@vitest/spy" "3.2.4"
|
||||
esbuild "^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0"
|
||||
open "^10.2.0"
|
||||
recast "^0.23.5"
|
||||
semver "^7.6.2"
|
||||
use-sync-external-store "^1.5.0"
|
||||
|
|
@ -7088,9 +7148,9 @@ vite-plugin-node-polyfills@^0.24.0:
|
|||
node-stdlib-browser "^1.2.0"
|
||||
|
||||
vite@^7.1.9:
|
||||
version "7.2.4"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-7.2.4.tgz#a3a09c7e25487612ecc1119c7d412c73da35bd4e"
|
||||
integrity sha512-NL8jTlbo0Tn4dUEXEsUg8KeyG/Lkmc4Fnzb8JXN/Ykm9G4HNImjtABMJgkQoVjOBN/j2WAwDTRytdqJbZsah7w==
|
||||
version "7.2.7"
|
||||
resolved "https://registry.yarnpkg.com/vite/-/vite-7.2.7.tgz#0789a4c3206081699f34a9ecca2dda594a07478e"
|
||||
integrity sha512-ITcnkFeR3+fI8P1wMgItjGrR10170d8auB4EpMLPqmx6uxElH3a/hHGQabSHKdqd4FXWO1nFIp9rRn7JQ34ACQ==
|
||||
dependencies:
|
||||
esbuild "^0.25.0"
|
||||
fdir "^6.5.0"
|
||||
|
|
@ -7251,6 +7311,13 @@ ws@^8.18.0:
|
|||
resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.3.tgz#b56b88abffde62791c639170400c93dcb0c95472"
|
||||
integrity sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==
|
||||
|
||||
wsl-utils@^0.1.0:
|
||||
version "0.1.0"
|
||||
resolved "https://registry.yarnpkg.com/wsl-utils/-/wsl-utils-0.1.0.tgz#8783d4df671d4d50365be2ee4c71917a0557baab"
|
||||
integrity sha512-h3Fbisa2nKGPxCpm89Hk33lBLsnaGBvctQopaBSOW/uIs6FTe1ATyAnKFJrzVs9vpGdsTe73WF3V4lIsk4Gacw==
|
||||
dependencies:
|
||||
is-wsl "^3.1.0"
|
||||
|
||||
xml@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/xml/-/xml-1.0.1.tgz#78ba72020029c5bc87b8a81a3cfcd74b4a2fc1e5"
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ test.describe("Landmark navigation tests", () => {
|
|||
|
||||
// Pressing Control+F6 again will focus room search
|
||||
await page.keyboard.press("ControlOrMeta+F6");
|
||||
await expect(page.locator(".mx_RoomListSearch_search")).toBeFocused();
|
||||
await expect(page.locator("#room-list-search-button")).toBeFocused();
|
||||
|
||||
// Pressing Control+F6 again will focus the message composer
|
||||
await page.keyboard.press("ControlOrMeta+F6");
|
||||
|
|
@ -44,7 +44,7 @@ test.describe("Landmark navigation tests", () => {
|
|||
await expect(page.locator(".mx_HomePage")).toBeFocused();
|
||||
|
||||
await page.keyboard.press("ControlOrMeta+Shift+F6");
|
||||
await expect(page.locator(".mx_RoomListSearch_search")).toBeFocused();
|
||||
await expect(page.locator("#room-list-search-button")).toBeFocused();
|
||||
|
||||
await page.keyboard.press("ControlOrMeta+Shift+F6");
|
||||
await expect(page.locator(".mx_SpaceButton_active")).toBeFocused();
|
||||
|
|
@ -75,7 +75,7 @@ test.describe("Landmark navigation tests", () => {
|
|||
|
||||
// Pressing Control+F6 again will focus room search
|
||||
await page.keyboard.press("ControlOrMeta+F6");
|
||||
await expect(page.locator(".mx_RoomListSearch_search")).toBeFocused();
|
||||
await expect(page.locator("#room-list-search-button")).toBeFocused();
|
||||
|
||||
// Pressing Control+F6 again will focus the room tile in the room list
|
||||
await page.keyboard.press("ControlOrMeta+F6");
|
||||
|
|
@ -97,7 +97,7 @@ test.describe("Landmark navigation tests", () => {
|
|||
await expect(page.locator(".mx_RoomListItemView_selected")).toBeFocused();
|
||||
|
||||
await page.keyboard.press("ControlOrMeta+Shift+F6");
|
||||
await expect(page.locator(".mx_RoomListSearch_search")).toBeFocused();
|
||||
await expect(page.locator("#room-list-search-button")).toBeFocused();
|
||||
|
||||
await page.keyboard.press("ControlOrMeta+Shift+F6");
|
||||
await expect(page.locator(".mx_SpaceButton_active")).toBeFocused();
|
||||
|
|
@ -131,7 +131,7 @@ test.describe("Landmark navigation tests", () => {
|
|||
|
||||
// Pressing Control+F6 again will focus room search
|
||||
await page.keyboard.press("ControlOrMeta+F6");
|
||||
await expect(page.locator(".mx_RoomListSearch_search")).toBeFocused();
|
||||
await expect(page.locator("#room-list-search-button")).toBeFocused();
|
||||
|
||||
// Pressing Control+F6 again will focus the room tile in the room list
|
||||
await page.keyboard.press("ControlOrMeta+F6");
|
||||
|
|
@ -153,7 +153,7 @@ test.describe("Landmark navigation tests", () => {
|
|||
await expect(page.locator(".mx_RoomListItemView")).toBeFocused();
|
||||
|
||||
await page.keyboard.press("ControlOrMeta+Shift+F6");
|
||||
await expect(page.locator(".mx_RoomListSearch_search")).toBeFocused();
|
||||
await expect(page.locator("#room-list-search-button")).toBeFocused();
|
||||
|
||||
await page.keyboard.press("ControlOrMeta+Shift+F6");
|
||||
await expect(page.locator(".mx_SpaceButton_active")).toBeFocused();
|
||||
|
|
|
|||
|
|
@ -351,7 +351,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
|||
const composer = thread.locator(".mx_MessageComposer--compact");
|
||||
// Assert that the reply preview contains audio ReplyTile the file info button
|
||||
await expect(
|
||||
composer.locator(".mx_ReplyPreview .mx_ReplyTile_audio .mx_MFileBody_info[role='button']"),
|
||||
composer.locator(".mx_ReplyPreview .mx_ReplyTile .mx_MFileBody_info[role='button']"),
|
||||
).toBeVisible();
|
||||
|
||||
// Select :smile: emoji and send it
|
||||
|
|
@ -360,6 +360,6 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
|||
await composer.getByTestId("basicmessagecomposer").press("Enter");
|
||||
|
||||
// Assert that the file name is rendered on the file button
|
||||
await expect(threadTile.locator(".mx_ReplyTile_audio .mx_MFileBody_info[role='button']")).toBeVisible();
|
||||
await expect(threadTile.locator(".mx_ReplyTile .mx_MFileBody_info[role='button']")).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -31,15 +31,11 @@ const startDMWithBob = async (page: Page, bob: Bot) => {
|
|||
|
||||
const testMessages = async (page: Page, bob: Bot, bobRoomId: string) => {
|
||||
// check the invite message
|
||||
await expect(
|
||||
page.locator(".mx_EventTile", { hasText: "Hey!" }).locator(".mx_EventTile_e2eIcon_warning"),
|
||||
).not.toBeVisible();
|
||||
await expect(page.locator(".mx_EventTile", { hasText: "Hey!" }).locator(".mx_EventTile_e2eIcon")).not.toBeVisible();
|
||||
|
||||
// Bob sends a response
|
||||
await bob.sendMessage(bobRoomId, "Hoo!");
|
||||
await expect(
|
||||
page.locator(".mx_EventTile", { hasText: "Hoo!" }).locator(".mx_EventTile_e2eIcon_warning"),
|
||||
).not.toBeVisible();
|
||||
await expect(page.locator(".mx_EventTile", { hasText: "Hoo!" }).locator(".mx_EventTile_e2eIcon")).not.toBeVisible();
|
||||
};
|
||||
|
||||
const bobJoin = async (page: Page, bob: Bot) => {
|
||||
|
|
|
|||
|
|
@ -30,69 +30,80 @@ test.describe("Cryptography", function () {
|
|||
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 ({
|
||||
homeserver,
|
||||
page,
|
||||
app,
|
||||
credentials,
|
||||
user,
|
||||
}) => {
|
||||
test.setTimeout(60000);
|
||||
test(
|
||||
"should handle device-relative historical messages",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ homeserver, page, app, credentials, user }) => {
|
||||
test.setTimeout(60000);
|
||||
|
||||
// Start with a logged-in session, without key backup, and send a message.
|
||||
await createRoom(page, "Test room", true);
|
||||
await sendMessageInCurrentRoom(page, "test test");
|
||||
// Start with a logged-in session, without key backup, and send a message.
|
||||
await createRoom(page, "Test room", true);
|
||||
await sendMessageInCurrentRoom(page, "test test");
|
||||
|
||||
// Log out, discarding the key for the sent message.
|
||||
await logOutOfElement(page, true);
|
||||
// Log out, discarding the key for the sent message.
|
||||
await logOutOfElement(page, true);
|
||||
|
||||
// Log in again, and see how the message looks.
|
||||
await logIntoElement(page, credentials);
|
||||
await app.viewRoomByName("Test room");
|
||||
const lastTile = page.locator(".mx_EventTile").last();
|
||||
await expect(lastTile).toContainText("Historical messages are not available on this device");
|
||||
await expect(lastTile.locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible();
|
||||
// Log in again, and see how the message looks.
|
||||
await logIntoElement(page, credentials);
|
||||
await app.viewRoomByName("Test room");
|
||||
const lastTile = page.locator(".mx_EventTile").last();
|
||||
await expect(lastTile).toContainText("Historical messages are not available on this device");
|
||||
await expect(lastTile.locator(".mx_EventTile_e2eIcon")).toHaveAccessibleName(
|
||||
"This message could not be decrypted",
|
||||
);
|
||||
await expect(lastTile).toMatchScreenshot("history-not-available.png", {
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
});
|
||||
|
||||
// Now, we set up key backup, and then send another message.
|
||||
const secretStorageKey = await enableKeyBackup(app);
|
||||
await app.viewRoomByName("Test room");
|
||||
await sendMessageInCurrentRoom(page, "test2 test2");
|
||||
// Now, we set up key backup, and then send another message.
|
||||
const secretStorageKey = await enableKeyBackup(app);
|
||||
await app.viewRoomByName("Test room");
|
||||
await sendMessageInCurrentRoom(page, "test2 test2");
|
||||
|
||||
// Workaround for https://github.com/element-hq/element-web/issues/27267. It can take up to 10 seconds for
|
||||
// the key to be backed up.
|
||||
await page.waitForTimeout(10000);
|
||||
// Workaround for https://github.com/element-hq/element-web/issues/27267. It can take up to 10 seconds for
|
||||
// the key to be backed up.
|
||||
await page.waitForTimeout(10000);
|
||||
|
||||
// Finally, log out again, and back in, skipping verification for now, and see what we see.
|
||||
await logOutOfElement(page);
|
||||
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: "I'll verify later" }).click();
|
||||
await app.viewRoomByName("Test room");
|
||||
// Finally, log out again, and back in, skipping verification for now, and see what we see.
|
||||
await logOutOfElement(page);
|
||||
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: "I'll verify later" }).click();
|
||||
await app.viewRoomByName("Test room");
|
||||
|
||||
// In this case, the call to cryptoApi.isEncryptionEnabledInRoom is taking a long time to resolve
|
||||
await page.waitForTimeout(1000);
|
||||
// In this case, the call to cryptoApi.isEncryptionEnabledInRoom is taking a long time to resolve
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// There should be two historical events in the timeline
|
||||
const tiles = await page.locator(".mx_EventTile").all();
|
||||
expect(tiles.length).toBeGreaterThanOrEqual(2);
|
||||
// look at the last two tiles only
|
||||
for (const tile of tiles.slice(-2)) {
|
||||
await expect(tile).toContainText("You need to verify this device for access to historical messages");
|
||||
await expect(tile.locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible();
|
||||
}
|
||||
// There should be two historical events in the timeline
|
||||
const tiles = await page.locator(".mx_EventTile").all();
|
||||
expect(tiles.length).toBeGreaterThanOrEqual(2);
|
||||
// look at the last two tiles only
|
||||
for (const tile of tiles.slice(-2)) {
|
||||
await expect(tile).toContainText(
|
||||
"You need to verify this device for access to historical messages",
|
||||
);
|
||||
await expect(tile.locator(".mx_EventTile_e2eIcon")).toHaveAccessibleName(
|
||||
"This message could not be decrypted",
|
||||
);
|
||||
}
|
||||
|
||||
// Now verify our device (setting up key backup), and check what happens
|
||||
await verifySession(app, secretStorageKey);
|
||||
const tilesAfterVerify = (await page.locator(".mx_EventTile").all()).slice(-2);
|
||||
// Now verify our device (setting up key backup), and check what happens
|
||||
await verifySession(app, secretStorageKey);
|
||||
const tilesAfterVerify = (await page.locator(".mx_EventTile").all()).slice(-2);
|
||||
|
||||
// The first message still cannot be decrypted, because it was never backed up. It's now a regular UTD though.
|
||||
await expect(tilesAfterVerify[0]).toContainText("Unable to decrypt message");
|
||||
await expect(tilesAfterVerify[0].locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible();
|
||||
// The first message still cannot be decrypted, because it was never backed up. It's now a regular UTD though.
|
||||
await expect(tilesAfterVerify[0]).toContainText("Unable to decrypt message");
|
||||
await expect(tilesAfterVerify[0].locator(".mx_EventTile_e2eIcon")).toHaveAccessibleName(
|
||||
"This message could not be decrypted",
|
||||
);
|
||||
|
||||
// The second message should now be decrypted, with a grey shield
|
||||
await expect(tilesAfterVerify[1]).toContainText("test2 test2");
|
||||
await expect(tilesAfterVerify[1].locator(".mx_EventTile_e2eIcon_normal")).toBeVisible();
|
||||
});
|
||||
// The second message should now be decrypted, with a grey shield
|
||||
await expect(tilesAfterVerify[1]).toContainText("test2 test2");
|
||||
await expect(tilesAfterVerify[1].locator(".mx_EventTile_e2eIcon")).toHaveAccessibleName(
|
||||
"The authenticity of this encrypted message can't be guaranteed on this device.",
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
test.describe("non-joined historical messages", () => {
|
||||
test.skip(isDendrite, "does not yet support membership on events");
|
||||
|
|
@ -186,7 +197,9 @@ test.describe("Cryptography", function () {
|
|||
// The first message from Bob was sent before Alice was in the room, so should
|
||||
// be different from the standard UTD message
|
||||
await expect(tiles[tiles.length - 5]).toContainText("You don't have access to this message");
|
||||
await expect(tiles[tiles.length - 5].locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible();
|
||||
await expect(tiles[tiles.length - 5].locator(".mx_EventTile_e2eIcon")).toHaveAccessibleName(
|
||||
"This message could not be decrypted",
|
||||
);
|
||||
|
||||
// The second message from Bob should be decryptable
|
||||
await expect(tiles[tiles.length - 2]).toContainText("This should be decryptable");
|
||||
|
|
@ -196,7 +209,9 @@ test.describe("Cryptography", function () {
|
|||
// in the room and is expected to be decryptable, so this should have the
|
||||
// standard UTD message
|
||||
await expect(tiles[tiles.length - 1]).toContainText("Unable to decrypt message");
|
||||
await expect(tiles[tiles.length - 1].locator(".mx_EventTile_e2eIcon_decryption_failure")).toBeVisible();
|
||||
await expect(tiles[tiles.length - 1].locator(".mx_EventTile_e2eIcon")).toHaveAccessibleName(
|
||||
"This message could not be decrypted",
|
||||
);
|
||||
});
|
||||
|
||||
test("should be able to jump to a message sent before our last join event", async ({
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
|||
await doTwoWaySasVerification(page, verifier);
|
||||
|
||||
await infoDialog.getByRole("button", { name: "They match" }).click();
|
||||
await expect(page.locator(".mx_E2EIcon_verified")).toMatchScreenshot("device-verified-e2eIcon.png");
|
||||
await expect(page.locator(".mx_E2EIcon")).toMatchScreenshot("device-verified-e2eIcon.png");
|
||||
await infoDialog.getByRole("button", { name: "Got it" }).click();
|
||||
|
||||
// Check that our device is now cross-signed
|
||||
|
|
@ -130,53 +130,68 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
|||
await page.unrouteAll({ behavior: "ignoreErrors" });
|
||||
});
|
||||
|
||||
test("Verify device with QR code during login", async ({ page, app, credentials, homeserver }) => {
|
||||
// A mode 0x02 verification: "self-verifying in which the current device does not yet trust the master key"
|
||||
await logIntoElement(page, credentials);
|
||||
test(
|
||||
"Verify device with QR code during login",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, credentials, homeserver }) => {
|
||||
// A mode 0x02 verification: "self-verifying in which the current device does not yet trust the master key"
|
||||
await logIntoElement(page, credentials);
|
||||
|
||||
// Launch the verification request between alice and the bot
|
||||
const verificationRequest = await initiateAliceVerificationRequest(page);
|
||||
// Launch the verification request between alice and the bot
|
||||
const verificationRequest = await initiateAliceVerificationRequest(page);
|
||||
|
||||
const infoDialog = page.locator(".mx_InfoDialog");
|
||||
// feed the QR code into the verification request.
|
||||
const qrData = await readQrCode(infoDialog);
|
||||
const verifier = await verificationRequest.evaluateHandle(
|
||||
(request, qrData) => request.scanQRCode(new Uint8ClampedArray(qrData)),
|
||||
[...qrData],
|
||||
);
|
||||
const infoDialog = page.locator(".mx_InfoDialog");
|
||||
// feed the QR code into the verification request.
|
||||
const qrData = await readQrCode(infoDialog);
|
||||
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("qr-code.png", {
|
||||
mask: [infoDialog.locator("img")],
|
||||
});
|
||||
const verifier = await verificationRequest.evaluateHandle(
|
||||
(request, qrData) => request.scanQRCode(new Uint8ClampedArray(qrData)),
|
||||
[...qrData],
|
||||
);
|
||||
|
||||
// Confirm that the bot user scanned successfully
|
||||
await expect(infoDialog.getByText("Confirm that you see a green shield on your other device")).toBeVisible();
|
||||
await infoDialog.getByRole("button", { name: "Yes, I see a green shield" }).click();
|
||||
await infoDialog.getByRole("button", { name: "Got it" }).click();
|
||||
// Confirm that the bot user scanned successfully
|
||||
await expect(
|
||||
infoDialog.getByText("Confirm that you see a green shield on your other device"),
|
||||
).toBeVisible();
|
||||
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("confirm-green-shield.png");
|
||||
await infoDialog.getByRole("button", { name: "Yes, I see a green shield" }).click();
|
||||
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("got-it.png");
|
||||
await infoDialog.getByRole("button", { name: "Got it" }).click();
|
||||
|
||||
// wait for the bot to see we have finished
|
||||
await verifier.evaluate((verifier) => verifier.verify());
|
||||
// wait for the bot to see we have finished
|
||||
await verifier.evaluate((verifier) => verifier.verify());
|
||||
|
||||
// the bot uploads the signatures asynchronously, so wait for that to happen
|
||||
await page.waitForTimeout(1000);
|
||||
// the bot uploads the signatures asynchronously, so wait for that to happen
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
// our device should trust the bot device
|
||||
await app.client.evaluate(async (cli, aliceBotCredentials) => {
|
||||
const deviceStatus = await cli
|
||||
.getCrypto()!
|
||||
.getDeviceVerificationStatus(aliceBotCredentials.userId, aliceBotCredentials.deviceId);
|
||||
if (!deviceStatus.isVerified()) {
|
||||
throw new Error("Bot device was not verified after QR code verification");
|
||||
}
|
||||
}, aliceBotClient.credentials);
|
||||
// our device should trust the bot device
|
||||
await app.client.evaluate(async (cli, aliceBotCredentials) => {
|
||||
const deviceStatus = await cli
|
||||
.getCrypto()!
|
||||
.getDeviceVerificationStatus(aliceBotCredentials.userId, aliceBotCredentials.deviceId);
|
||||
if (!deviceStatus.isVerified()) {
|
||||
throw new Error("Bot device was not verified after QR code verification");
|
||||
}
|
||||
}, aliceBotClient.credentials);
|
||||
|
||||
// Check that our device is now cross-signed
|
||||
await checkDeviceIsCrossSigned(app);
|
||||
// Check that our device is now cross-signed
|
||||
await checkDeviceIsCrossSigned(app);
|
||||
|
||||
// Check that the current device is connected to key backup
|
||||
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
|
||||
});
|
||||
// Check that the current device is connected to key backup
|
||||
await checkDeviceIsConnectedKeyBackup(app, expectedBackupVersion, true);
|
||||
},
|
||||
);
|
||||
|
||||
test("Verify device with Security Phrase during login", async ({ page, app, credentials, homeserver }) => {
|
||||
await logIntoElement(page, credentials);
|
||||
await enterRecoveryKeyAndCheckVerified(page, app, "new passphrase");
|
||||
});
|
||||
test(
|
||||
"Verify device with Security Phrase during login",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, credentials, homeserver }) => {
|
||||
await logIntoElement(page, credentials);
|
||||
await enterRecoveryKeyAndCheckVerified(page, app, "new passphrase", true);
|
||||
},
|
||||
);
|
||||
|
||||
test("Verify device with Recovery Key during login", async ({ page, app, credentials, homeserver }) => {
|
||||
const recoveryKey = (await aliceBotClient.getRecoveryKey()).encodedPrivateKey;
|
||||
|
|
@ -226,7 +241,12 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
|||
});
|
||||
|
||||
/** Helper for the three tests above which verify by recovery key */
|
||||
async function enterRecoveryKeyAndCheckVerified(page: Page, app: ElementAppPage, recoveryKey: string) {
|
||||
async function enterRecoveryKeyAndCheckVerified(
|
||||
page: Page,
|
||||
app: ElementAppPage,
|
||||
recoveryKey: string,
|
||||
screenshot = false,
|
||||
) {
|
||||
await page.getByRole("button", { name: "Use recovery key" }).click();
|
||||
|
||||
// Enter the recovery key
|
||||
|
|
@ -234,8 +254,12 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
|||
// We use `pressSequentially` here to make sure that the FocusLock isn't causing us any problems
|
||||
// (cf https://github.com/element-hq/element-web/issues/30089)
|
||||
await dialog.getByTitle("Recovery key").pressSequentially(recoveryKey);
|
||||
if (screenshot) {
|
||||
await expect(page.locator(".mx_Dialog").filter({ hasText: "Enter your recovery key" })).toMatchScreenshot(
|
||||
"recovery-key.png",
|
||||
);
|
||||
}
|
||||
await dialog.getByRole("button", { name: "Continue", disabled: false }).click();
|
||||
|
||||
await page.getByRole("button", { name: "Done" }).click();
|
||||
|
||||
// Check that our device is now cross-signed
|
||||
|
|
|
|||
|
|
@ -77,11 +77,8 @@ test.describe("Cryptography", function () {
|
|||
const last = page.locator(".mx_EventTile_last");
|
||||
await expect(last).toContainText("Unable to decrypt message");
|
||||
const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon");
|
||||
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_decryption_failure/);
|
||||
await lastE2eIcon.focus();
|
||||
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
|
||||
"This message could not be decrypted",
|
||||
);
|
||||
await expect(lastE2eIcon).toHaveAccessibleName("This message could not be decrypted");
|
||||
await expect(lastE2eIcon).toMatchScreenshot("event-shield-utd.png");
|
||||
|
||||
/* Should show a red padlock for an unencrypted message in an e2e room */
|
||||
await bob.evaluate(
|
||||
|
|
@ -99,10 +96,8 @@ test.describe("Cryptography", function () {
|
|||
);
|
||||
|
||||
await expect(last).toContainText("test unencrypted");
|
||||
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
||||
await expect(lastE2eIcon).toHaveAccessibleName("Not encrypted");
|
||||
await expect(lastE2eIcon).toMatchScreenshot("event-shield-warning.png");
|
||||
await lastE2eIcon.focus();
|
||||
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText("Not encrypted");
|
||||
|
||||
/* Should show no padlock for an unverified user */
|
||||
// bob sends a valid event
|
||||
|
|
@ -133,11 +128,8 @@ test.describe("Cryptography", function () {
|
|||
/* should show red padlock for a message from an unverified device */
|
||||
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from unverified");
|
||||
await expect(lastTile).toContainText("test encrypted from unverified");
|
||||
await expect(lastTileE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
||||
await lastTileE2eIcon.focus();
|
||||
await expect(await app.getTooltipForElement(lastTileE2eIcon)).toContainText(
|
||||
"Encrypted by a device not verified by its owner.",
|
||||
);
|
||||
await expect(lastTileE2eIcon).toHaveAccessibleName("Encrypted by a device not verified by its owner.");
|
||||
await expect(lastE2eIcon).toMatchScreenshot("event-shield-not-verified.png");
|
||||
|
||||
/* Should show a red padlock for a message from an unverified device.
|
||||
* Rust crypto remembers the verification state of the sending device, so it will know that the device was
|
||||
|
|
@ -153,64 +145,58 @@ test.describe("Cryptography", function () {
|
|||
await app.viewRoomByName("TestRoom");
|
||||
|
||||
await expect(last).toContainText("test encrypted from unverified");
|
||||
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
||||
await lastE2eIcon.focus();
|
||||
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
|
||||
"Encrypted by a device not verified by its owner.",
|
||||
);
|
||||
await expect(lastE2eIcon).toHaveAccessibleName("Encrypted by a device not verified by its owner.");
|
||||
await expect(lastE2eIcon).toMatchScreenshot("event-shield-not-verified.png");
|
||||
},
|
||||
);
|
||||
|
||||
test("Should show a grey padlock for a key restored from backup", async ({
|
||||
page,
|
||||
app,
|
||||
bot: bob,
|
||||
homeserver,
|
||||
user: aliceCredentials,
|
||||
}) => {
|
||||
test.slow();
|
||||
const securityKey = await enableKeyBackup(app);
|
||||
test(
|
||||
"Should show a grey padlock for a key restored from backup",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, bot: bob, homeserver, user: aliceCredentials }) => {
|
||||
test.slow();
|
||||
const securityKey = await enableKeyBackup(app);
|
||||
|
||||
// bob sends a valid event
|
||||
await bob.sendMessage(testRoomId, "test encrypted 1");
|
||||
// bob sends a valid event
|
||||
await bob.sendMessage(testRoomId, "test encrypted 1");
|
||||
|
||||
const lastTile = page.locator(".mx_EventTile_last");
|
||||
const lastTileE2eIcon = lastTile.locator(".mx_EventTile_e2eIcon");
|
||||
await expect(lastTile).toContainText("test encrypted 1");
|
||||
// no e2e icon
|
||||
await expect(lastTileE2eIcon).not.toBeVisible();
|
||||
const lastTile = page.locator(".mx_EventTile_last");
|
||||
const lastTileE2eIcon = lastTile.locator(".mx_EventTile_e2eIcon");
|
||||
await expect(lastTile).toContainText("test encrypted 1");
|
||||
// no e2e icon
|
||||
await expect(lastTileE2eIcon).not.toBeVisible();
|
||||
|
||||
// Workaround for https://github.com/element-hq/element-web/issues/27267. It can take up to 10 seconds for
|
||||
// the key to be backed up.
|
||||
await page.waitForTimeout(10000);
|
||||
// Workaround for https://github.com/element-hq/element-web/issues/27267. It can take up to 10 seconds for
|
||||
// the key to be backed up.
|
||||
await page.waitForTimeout(10000);
|
||||
|
||||
/* log out, and back in */
|
||||
await logOutOfElement(page);
|
||||
// Reload to work around a Rust crypto bug where it can hold onto the indexeddb even after logout
|
||||
// https://github.com/element-hq/element-web/issues/25779
|
||||
await page.addInitScript(() => {
|
||||
// When we reload, the initScript created by the `user`/`pageWithCredentials` fixtures
|
||||
// will re-inject the original credentials into localStorage, which we don't want.
|
||||
// To work around, we add a second initScript which will clear localStorage again.
|
||||
window.localStorage.clear();
|
||||
});
|
||||
await page.reload();
|
||||
await logIntoElementAndVerify(page, aliceCredentials, securityKey);
|
||||
/* log out, and back in */
|
||||
await logOutOfElement(page);
|
||||
// Reload to work around a Rust crypto bug where it can hold onto the indexeddb even after logout
|
||||
// https://github.com/element-hq/element-web/issues/25779
|
||||
await page.addInitScript(() => {
|
||||
// When we reload, the initScript created by the `user`/`pageWithCredentials` fixtures
|
||||
// will re-inject the original credentials into localStorage, which we don't want.
|
||||
// To work around, we add a second initScript which will clear localStorage again.
|
||||
window.localStorage.clear();
|
||||
});
|
||||
await page.reload();
|
||||
await logIntoElementAndVerify(page, aliceCredentials, securityKey);
|
||||
|
||||
/* go back to the test room and find Bob's message again */
|
||||
await app.viewRoomById(testRoomId);
|
||||
await expect(lastTile).toContainText("test encrypted 1");
|
||||
// The gray shield would be a mx_EventTile_e2eIcon_normal. The red shield would be a mx_EventTile_e2eIcon_warning.
|
||||
// No shield would have no div mx_EventTile_e2eIcon at all.
|
||||
await expect(lastTileE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_normal/);
|
||||
await lastTileE2eIcon.hover();
|
||||
// The key is coming from backup, so it is not anymore possible to establish if the claimed device
|
||||
// creator of this key is authentic. The tooltip should be "The authenticity of this encrypted message can't be guaranteed on this device."
|
||||
// It is not "Encrypted by an unknown or deleted device." even if the claimed device is actually deleted.
|
||||
await expect(await app.getTooltipForElement(lastTileE2eIcon)).toContainText(
|
||||
"The authenticity of this encrypted message can't be guaranteed on this device.",
|
||||
);
|
||||
});
|
||||
/* go back to the test room and find Bob's message again */
|
||||
await app.viewRoomById(testRoomId);
|
||||
await expect(lastTile).toContainText("test encrypted 1");
|
||||
// The gray shield would be a Compound info icon. The red shield would be a Compound error solid icon.
|
||||
// No shield would have no div mx_EventTile_e2eIcon at all.
|
||||
// The key is coming from backup, so it is not anymore possible to establish if the claimed device
|
||||
// creator of this key is authentic. The tooltip should be "The authenticity of this encrypted message can't be guaranteed on this device."
|
||||
// It is not "Encrypted by an unknown or deleted device." even if the claimed device is actually deleted.
|
||||
await expect(lastTileE2eIcon).toHaveAccessibleName(
|
||||
"The authenticity of this encrypted message can't be guaranteed on this device.",
|
||||
);
|
||||
await expect(lastTileE2eIcon).toMatchScreenshot("event-shield-authenticity.png");
|
||||
},
|
||||
);
|
||||
|
||||
test("should show the correct shield on edited e2e events", async ({ page, app, bot: bob, homeserver }) => {
|
||||
// bob has a second, not cross-signed, device
|
||||
|
|
@ -224,7 +210,7 @@ test.describe("Cryptography", function () {
|
|||
|
||||
// the message should appear, decrypted, with no warning
|
||||
await expect(
|
||||
page.locator(".mx_EventTile", { hasText: "Hoo!" }).locator(".mx_EventTile_e2eIcon_warning"),
|
||||
page.locator(".mx_EventTile", { hasText: "Hoo!" }).locator(".mx_EventTile_e2eIcon"),
|
||||
).not.toBeVisible();
|
||||
|
||||
// bob sends an edit to the first message with his unverified device
|
||||
|
|
@ -241,7 +227,7 @@ test.describe("Cryptography", function () {
|
|||
|
||||
// the edit should have a warning
|
||||
await expect(
|
||||
page.locator(".mx_EventTile", { hasText: "Haa!" }).locator(".mx_EventTile_e2eIcon_warning"),
|
||||
page.locator(".mx_EventTile", { hasText: "Haa!" }).locator(".mx_EventTile_e2eIcon"),
|
||||
).toBeVisible();
|
||||
|
||||
// a second edit from the verified device should be ok
|
||||
|
|
@ -257,77 +243,69 @@ test.describe("Cryptography", function () {
|
|||
});
|
||||
|
||||
await expect(
|
||||
page.locator(".mx_EventTile", { hasText: "Hee!" }).locator(".mx_EventTile_e2eIcon_warning"),
|
||||
page.locator(".mx_EventTile", { hasText: "Hee!" }).locator(".mx_EventTile_e2eIcon"),
|
||||
).not.toBeVisible();
|
||||
});
|
||||
|
||||
test("should show correct shields on events sent by devices which have since been deleted", async ({
|
||||
page,
|
||||
app,
|
||||
bot: bob,
|
||||
homeserver,
|
||||
}) => {
|
||||
// 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
|
||||
// his user info.
|
||||
await waitForDevices(app, bob.credentials.userId, 1);
|
||||
test(
|
||||
"should show correct shields on events sent by devices which have since been deleted",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, bot: bob, homeserver }) => {
|
||||
// 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
|
||||
// his user info.
|
||||
await waitForDevices(app, bob.credentials.userId, 1);
|
||||
|
||||
// Our app is blocked from syncing while Bob sends his messages.
|
||||
await app.client.network.goOffline();
|
||||
// Our app is blocked from syncing while Bob sends his messages.
|
||||
await app.client.network.goOffline();
|
||||
|
||||
// Bob sends a message from his verified device
|
||||
await bob.sendMessage(testRoomId, "test encrypted from verified");
|
||||
// Bob sends a message from his verified device
|
||||
await bob.sendMessage(testRoomId, "test encrypted from verified");
|
||||
|
||||
// And one from a second, not cross-signed, device
|
||||
const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob);
|
||||
await bobSecondDevice.waitForNextSync(); // make sure the client knows the room is encrypted
|
||||
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from unverified");
|
||||
// And one from a second, not cross-signed, device
|
||||
const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob);
|
||||
await bobSecondDevice.waitForNextSync(); // make sure the client knows the room is encrypted
|
||||
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from unverified");
|
||||
|
||||
// ... and then logs out both devices.
|
||||
await bob.evaluate((cli) => cli.logout(true));
|
||||
await bobSecondDevice.evaluate((cli) => cli.logout(true));
|
||||
// ... and then logs out both devices.
|
||||
await bob.evaluate((cli) => cli.logout(true));
|
||||
await bobSecondDevice.evaluate((cli) => cli.logout(true));
|
||||
|
||||
// Let our app start syncing again
|
||||
await app.client.network.goOnline();
|
||||
// Let our app start syncing again
|
||||
await app.client.network.goOnline();
|
||||
|
||||
// Wait for the messages to arrive. It can take quite a while for the sync to wake up.
|
||||
const last = page.locator(".mx_EventTile_last");
|
||||
await expect(last).toContainText("test encrypted from unverified", { timeout: 20000 });
|
||||
const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon");
|
||||
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
||||
await lastE2eIcon.focus();
|
||||
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
|
||||
"Encrypted by a device not verified by its owner.",
|
||||
);
|
||||
// Wait for the messages to arrive. It can take quite a while for the sync to wake up.
|
||||
const last = page.locator(".mx_EventTile_last");
|
||||
await expect(last).toContainText("test encrypted from unverified", { timeout: 20000 });
|
||||
const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon");
|
||||
await expect(lastE2eIcon).toHaveAccessibleName("Encrypted by a device not verified by its owner.");
|
||||
await expect(lastE2eIcon).toMatchScreenshot("event-shield-not-verified.png");
|
||||
|
||||
const penultimate = page.locator(".mx_EventTile").filter({ hasText: "test encrypted from verified" });
|
||||
await assertNoE2EIcon(penultimate, app);
|
||||
});
|
||||
const penultimate = page.locator(".mx_EventTile").filter({ hasText: "test encrypted from verified" });
|
||||
await assertNoE2EIcon(penultimate, app);
|
||||
},
|
||||
);
|
||||
|
||||
test("should show correct shields on events sent by users with changed identity", async ({
|
||||
page,
|
||||
app,
|
||||
bot: bob,
|
||||
homeserver,
|
||||
}) => {
|
||||
// Verify Bob
|
||||
await verify(app, bob);
|
||||
test(
|
||||
"should show correct shields on events sent by users with changed identity",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, bot: bob, homeserver }) => {
|
||||
// Verify Bob
|
||||
await verify(app, bob);
|
||||
|
||||
// Bob logs in a new device and resets cross-signing
|
||||
const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob);
|
||||
await bootstrapCrossSigningForClient(await bobSecondDevice.prepareClient(), bob.credentials, true);
|
||||
// Bob logs in a new device and resets cross-signing
|
||||
const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob);
|
||||
await bootstrapCrossSigningForClient(await bobSecondDevice.prepareClient(), bob.credentials, true);
|
||||
|
||||
/* should show an error for a message from a previously verified device */
|
||||
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from user that was previously verified");
|
||||
const last = page.locator(".mx_EventTile_last");
|
||||
await expect(last).toContainText("test encrypted from user that was previously verified");
|
||||
const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon");
|
||||
await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/);
|
||||
await lastE2eIcon.focus();
|
||||
await expect(await app.getTooltipForElement(lastE2eIcon)).toContainText(
|
||||
"Sender's verified identity was reset",
|
||||
);
|
||||
});
|
||||
/* should show an error for a message from a previously verified device */
|
||||
await bobSecondDevice.sendMessage(testRoomId, "test encrypted from user that was previously verified");
|
||||
const last = page.locator(".mx_EventTile_last");
|
||||
await expect(last).toContainText("test encrypted from user that was previously verified");
|
||||
const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon");
|
||||
await expect(lastE2eIcon).toHaveAccessibleName("Sender's verified identity was reset");
|
||||
await expect(lastE2eIcon).toMatchScreenshot("event-shield-identity-reset.png");
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -343,8 +321,6 @@ async function assertNoE2EIcon(messageLocator: Locator, app: ElementAppPage) {
|
|||
const e2eIcon = messageLocator.locator(".mx_EventTile_e2eIcon");
|
||||
if ((await e2eIcon.count()) > 0) {
|
||||
// uh-oh, there is an e2e icon. Let's find out what it's about so that we can throw a helpful error.
|
||||
await e2eIcon.focus();
|
||||
const tooltip = await app.getTooltipForElement(e2eIcon);
|
||||
throw new Error(`Found an unexpected e2eIcon with tooltip '${await tooltip.textContent()}'`);
|
||||
await expect(e2eIcon).toHaveAccessibleName("None");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ export const test = base.extend<{
|
|||
room1Name: "Room 1",
|
||||
room1: async ({ room1Name: name, app, user, bot }, use) => {
|
||||
const roomId = await app.client.createRoom({ name, invite: [bot.credentials.userId] });
|
||||
await bot.awaitRoomMembership(roomId);
|
||||
await use({ name, roomId });
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -36,11 +36,13 @@ export const test = base.extend<{
|
|||
roomAlphaName: "Room Alpha",
|
||||
roomAlpha: async ({ roomAlphaName: name, app, user, bot }, use) => {
|
||||
const roomId = await app.client.createRoom({ name, invite: [bot.credentials.userId] });
|
||||
await bot.awaitRoomMembership(roomId);
|
||||
await use({ name, roomId });
|
||||
},
|
||||
roomBetaName: "Room Beta",
|
||||
roomBeta: async ({ roomBetaName: name, app, user, bot }, use) => {
|
||||
const roomId = await app.client.createRoom({ name, invite: [bot.credentials.userId] });
|
||||
await bot.awaitRoomMembership(roomId);
|
||||
await use({ name, roomId });
|
||||
},
|
||||
msg: async ({ page, app, util }, use) => {
|
||||
|
|
|
|||
|
|
@ -13,72 +13,30 @@ import { test } from ".";
|
|||
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
||||
test.describe("Message ordering", () => {
|
||||
test.describe("in the main timeline", () => {
|
||||
test.fixme(
|
||||
"A receipt for the last event in sync order (even with wrong ts) marks a room as read",
|
||||
() => {},
|
||||
);
|
||||
test.fixme(
|
||||
"A receipt for a non-last event in sync order (even when ts makes it last) leaves room unread",
|
||||
() => {},
|
||||
);
|
||||
test.fixme("A receipt for the last event in sync order (even with wrong ts) marks a room as read", () => {});
|
||||
test.fixme("A receipt for a non-last event in sync order (even when ts makes it last) leaves room unread", () => {});
|
||||
});
|
||||
|
||||
test.describe("in threads", () => {
|
||||
// These don't pass yet - we need MSC4033 - we don't even know the Sync order yet
|
||||
test.fixme(
|
||||
"A receipt for the last event in sync order (even with wrong ts) marks a thread as read",
|
||||
() => {},
|
||||
);
|
||||
test.fixme(
|
||||
"A receipt for a non-last event in sync order (even when ts makes it last) leaves thread unread",
|
||||
() => {},
|
||||
);
|
||||
test.fixme("A receipt for the last event in sync order (even with wrong ts) marks a thread as read", () => {});
|
||||
test.fixme("A receipt for a non-last event in sync order (even when ts makes it last) leaves thread unread", () => {});
|
||||
|
||||
// These pass now and should not later - we should use order from MSC4033 instead of ts
|
||||
// These are broken out
|
||||
test.fixme(
|
||||
"A receipt for last threaded event in ts order (even when it was received non-last) marks a thread as read",
|
||||
() => {},
|
||||
);
|
||||
test.fixme(
|
||||
"A receipt for non-last threaded event in ts order (even when it was received last) leaves thread unread",
|
||||
() => {},
|
||||
);
|
||||
test.fixme(
|
||||
"A receipt for last threaded edit in ts order (even when it was received non-last) marks a thread as read",
|
||||
() => {},
|
||||
);
|
||||
test.fixme(
|
||||
"A receipt for non-last threaded edit in ts order (even when it was received last) leaves thread unread",
|
||||
() => {},
|
||||
);
|
||||
test.fixme(
|
||||
"A receipt for last threaded reaction in ts order (even when it was received non-last) marks a thread as read",
|
||||
() => {},
|
||||
);
|
||||
test.fixme(
|
||||
"A receipt for non-last threaded reaction in ts order (even when it was received last) leaves thread unread",
|
||||
() => {},
|
||||
);
|
||||
test.fixme("A receipt for last threaded event in ts order (even when it was received non-last) marks a thread as read", () => {});
|
||||
test.fixme("A receipt for non-last threaded event in ts order (even when it was received last) leaves thread unread", () => {});
|
||||
test.fixme("A receipt for last threaded edit in ts order (even when it was received non-last) marks a thread as read", () => {});
|
||||
test.fixme("A receipt for non-last threaded edit in ts order (even when it was received last) leaves thread unread", () => {});
|
||||
test.fixme("A receipt for last threaded reaction in ts order (even when it was received non-last) marks a thread as read", () => {});
|
||||
test.fixme("A receipt for non-last threaded reaction in ts order (even when it was received last) leaves thread unread", () => {});
|
||||
});
|
||||
|
||||
test.describe("thread roots", () => {
|
||||
test.fixme(
|
||||
"A receipt for last reaction to thread root in sync order (even when ts makes it last) marks room as read",
|
||||
() => {},
|
||||
);
|
||||
test.fixme(
|
||||
"A receipt for non-last reaction to thread root in sync order (even when ts makes it last) leaves room unread",
|
||||
() => {},
|
||||
);
|
||||
test.fixme(
|
||||
"A receipt for last edit to thread root in sync order (even when ts makes it last) marks room as read",
|
||||
() => {},
|
||||
);
|
||||
test.fixme(
|
||||
"A receipt for non-last edit to thread root in sync order (even when ts makes it last) leaves room unread",
|
||||
() => {},
|
||||
);
|
||||
test.fixme("A receipt for last reaction to thread root in sync order (even when ts makes it last) marks room as read", () => {});
|
||||
test.fixme("A receipt for non-last reaction to thread root in sync order (even when ts makes it last) leaves room unread", () => {});
|
||||
test.fixme("A receipt for last edit to thread root in sync order (even when ts makes it last) marks room as read", () => {});
|
||||
test.fixme("A receipt for non-last edit to thread root in sync order (even when ts makes it last) leaves room unread", () => {});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,18 +12,20 @@ import { test } from ".";
|
|||
|
||||
test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
||||
test.describe("messages with missing referents", () => {
|
||||
test.fixme(
|
||||
"A message in an unknown thread is not visible and the room is read",
|
||||
async ({ roomAlpha: room1, roomBeta: room2, util, msg }) => {
|
||||
// Given a thread existed and the room is read
|
||||
await util.goTo(room1);
|
||||
await util.receiveMessages(room2, ["Root1", msg.threadedOff("Root1", "T1a")]);
|
||||
test.fixme("A message in an unknown thread is not visible and the room is read", async ({
|
||||
roomAlpha: room1,
|
||||
roomBeta: room2,
|
||||
util,
|
||||
msg,
|
||||
}) => {
|
||||
// Given a thread existed and the room is read
|
||||
await util.goTo(room1);
|
||||
await util.receiveMessages(room2, ["Root1", msg.threadedOff("Root1", "T1a")]);
|
||||
|
||||
// When I restart, forgetting the thread root
|
||||
// And I receive a message on that thread
|
||||
// Then the message is invisible and the room remains read
|
||||
},
|
||||
);
|
||||
// When I restart, forgetting the thread root
|
||||
// And I receive a message on that thread
|
||||
// Then the message is invisible and the room remains read
|
||||
});
|
||||
test.fixme("When a message's thread root appears later the thread appears and the room is unread", () => {});
|
||||
test.fixme("An edit of an unknown message is not visible and the room is read", () => {});
|
||||
test.fixme("When an edit's message appears later the edited version appears and the room is unread", () => {});
|
||||
|
|
|
|||
|
|
@ -14,14 +14,8 @@ test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
|||
test.describe("Notifications", () => {
|
||||
test.describe("in the main timeline", () => {
|
||||
test.fixme("A new message that mentions me shows a notification", () => {});
|
||||
test.fixme(
|
||||
"Reading a notifying message reduces the notification count in the room list, space and tab",
|
||||
() => {},
|
||||
);
|
||||
test.fixme(
|
||||
"Reading the last notifying message removes the notification marker from room list, space and tab",
|
||||
() => {},
|
||||
);
|
||||
test.fixme("Reading a notifying message reduces the notification count in the room list, space and tab", () => {});
|
||||
test.fixme("Reading the last notifying message removes the notification marker from room list, space and tab", () => {});
|
||||
test.fixme("Editing a message to mentions me shows a notification", () => {});
|
||||
test.fixme("Reading the last notifying edited message removes the notification marker", () => {});
|
||||
test.fixme("Redacting a notifying message removes the notification marker", () => {});
|
||||
|
|
@ -30,18 +24,9 @@ test.describe("Read receipts", { tag: "@mergequeue" }, () => {
|
|||
test.describe("in threads", () => {
|
||||
test.fixme("A new threaded message that mentions me shows a notification", () => {});
|
||||
test.fixme("Reading a notifying threaded message removes the notification count", () => {});
|
||||
test.fixme(
|
||||
"Notification count remains steady when reading threads that contain seen notifications",
|
||||
() => {},
|
||||
);
|
||||
test.fixme(
|
||||
"Notification count remains steady when paging up thread view even when threads contain seen notifications",
|
||||
() => {},
|
||||
);
|
||||
test.fixme(
|
||||
"Notification count remains steady when paging up thread view after mark as unread even if older threads contain notifications",
|
||||
() => {},
|
||||
);
|
||||
test.fixme("Notification count remains steady when reading threads that contain seen notifications", () => {});
|
||||
test.fixme("Notification count remains steady when paging up thread view even when threads contain seen notifications", () => {});
|
||||
test.fixme("Notification count remains steady when paging up thread view after mark as unread even if older threads contain notifications", () => {});
|
||||
test.fixme("Redacting a notifying threaded message removes the notification marker", () => {});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -25,7 +25,8 @@ test.describe("Release announcement", () => {
|
|||
labsFlags: ["feature_new_room_list"],
|
||||
});
|
||||
|
||||
test(
|
||||
// There is no release announcement currently live
|
||||
test.skip(
|
||||
"should display the new room list release announcement",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, room, util }) => {
|
||||
|
|
|
|||
|
|
@ -207,7 +207,7 @@ test.describe("RightPanel", () => {
|
|||
|
||||
// \d represents the number of the space members
|
||||
await page
|
||||
.locator(".mx_RoomInfoLine_private")
|
||||
.locator(".mx_RoomInfoLine")
|
||||
.getByRole("button", { name: /\d member/ })
|
||||
.click();
|
||||
await expect(page.locator(".mx_MemberListView")).toBeVisible();
|
||||
|
|
|
|||
|
|
@ -6,6 +6,8 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Com
|
|||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { type Page } from "playwright-core";
|
||||
|
||||
import { SettingLevel } from "../../../src/settings/SettingLevel";
|
||||
import { UIFeature } from "../../../src/settings/UIFeature";
|
||||
import { test, expect } from "../../element-web-test";
|
||||
|
|
@ -33,7 +35,10 @@ test.describe("Create Room", () => {
|
|||
axe.disableRules("color-contrast"); // XXX: Inheriting colour contrast issues from room view.
|
||||
await expect(axe).toHaveNoViolations();
|
||||
// Snapshot it
|
||||
await expect(dialog).toMatchScreenshot("create-room.png");
|
||||
// Mask topic to avoid flakiness with top border
|
||||
await expect(dialog).toMatchScreenshot("create-room.png", {
|
||||
mask: [dialog.locator(".mx_CreateRoomDialog_topic")],
|
||||
});
|
||||
|
||||
// Submit
|
||||
await dialog.getByRole("button", { name: "Create room" }).click();
|
||||
|
|
@ -72,7 +77,10 @@ test.describe("Create Room", () => {
|
|||
// Fill room address
|
||||
await dialog.getByRole("textbox", { name: "Room address" }).fill("test-create-room-video");
|
||||
// Snapshot it
|
||||
await expect(dialog).toMatchScreenshot("create-video-room.png");
|
||||
// Mask topic to avoid flakiness with top border
|
||||
await expect(dialog).toMatchScreenshot("create-video-room.png", {
|
||||
mask: [dialog.locator(".mx_CreateRoomDialog_topic")],
|
||||
});
|
||||
|
||||
// Submit
|
||||
await dialog.getByRole("button", { name: "Create video room" }).click();
|
||||
|
|
@ -100,7 +108,10 @@ test.describe("Create Room", () => {
|
|||
axe.disableRules("color-contrast"); // XXX: Inheriting colour contrast issues from room view.
|
||||
await expect(axe).toHaveNoViolations();
|
||||
// Snapshot it
|
||||
await expect(dialog).toMatchScreenshot("create-room-no-public.png");
|
||||
// Mask topic to avoid flakiness with top border
|
||||
await expect(dialog).toMatchScreenshot("create-room-no-public.png", {
|
||||
mask: [dialog.locator(".mx_CreateRoomDialog_topic")],
|
||||
});
|
||||
|
||||
// Submit
|
||||
await dialog.getByRole("button", { name: "Create room" }).click();
|
||||
|
|
@ -110,4 +121,107 @@ test.describe("Create Room", () => {
|
|||
await expect(header).toContainText(name);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("when the encrypted state labs flag is turned off", () => {
|
||||
test.use({ labsFlags: [] });
|
||||
|
||||
test("creates a room without encrypted state", { tag: "@screenshot" }, async ({ page, user: _user }) => {
|
||||
// When we start to create a room
|
||||
await page.getByRole("button", { name: "New conversation", exact: true }).click();
|
||||
await page.getByRole("menuitem", { name: "New room" }).click();
|
||||
await page.getByRole("textbox", { name: "Name" }).fill(name);
|
||||
|
||||
// Then there is no Encrypt state events button
|
||||
await expect(page.getByRole("checkbox", { name: "Encrypt state events" })).not.toBeVisible();
|
||||
|
||||
// And when we create the room
|
||||
await page.getByRole("button", { name: "Create room" }).click();
|
||||
|
||||
// Then we created a normal encrypted room, without encrypted state
|
||||
await expect(page.getByText("Encryption enabled")).toBeVisible();
|
||||
await expect(page.getByText("State encryption enabled")).not.toBeVisible();
|
||||
|
||||
// And the room name state event is not encrypted
|
||||
await viewSourceOnRoomNameEvent(page);
|
||||
await expect(page.getByText("Original event source")).toBeVisible();
|
||||
await expect(page.getByText("Decrypted event source")).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("when the encrypted state labs flag is turned on", () => {
|
||||
test.use({ labsFlags: ["feature_msc4362_encrypted_state_events"] });
|
||||
|
||||
test(
|
||||
"creates a room with encrypted state if we check the box",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, user: _user }) => {
|
||||
// Given we check the Encrypted State checkbox
|
||||
await page.getByRole("button", { name: "New conversation", exact: true }).click();
|
||||
await page.getByRole("menuitem", { name: "New room" }).click();
|
||||
await expect(page.getByRole("switch", { name: "Enable end-to-end encryption" })).toBeChecked();
|
||||
await page.getByRole("switch", { name: "Encrypt state events" }).click();
|
||||
await expect(page.getByRole("switch", { name: "Encrypt state events" })).toBeChecked();
|
||||
|
||||
// When we create a room
|
||||
await page.getByRole("textbox", { name: "Name" }).fill(name);
|
||||
await page.getByRole("button", { name: "Create room" }).click();
|
||||
|
||||
// Then we created an encrypted state room
|
||||
await expect(page.getByText("State encryption enabled")).toBeVisible();
|
||||
|
||||
// And it has the correct name
|
||||
await expect(page.getByTestId("timeline").getByRole("heading", { name })).toBeVisible();
|
||||
|
||||
// And the room name state event is encrypted
|
||||
await viewSourceOnRoomNameEvent(page);
|
||||
await expect(page.getByText("Decrypted event source")).toBeVisible();
|
||||
},
|
||||
);
|
||||
|
||||
test(
|
||||
"creates a room without encrypted state if we don't check the box",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, user: _user }) => {
|
||||
// Given we did not check the Encrypted State checkbox
|
||||
await page.getByRole("button", { name: "New conversation", exact: true }).click();
|
||||
await page.getByRole("menuitem", { name: "New room" }).click();
|
||||
await expect(page.getByRole("switch", { name: "Enable end-to-end encryption" })).toBeChecked();
|
||||
|
||||
// And it is off by default
|
||||
await expect(page.getByRole("switch", { name: "Encrypt state events" })).not.toBeChecked();
|
||||
|
||||
// When we create a room
|
||||
await page.getByRole("textbox", { name: "Name" }).fill(name);
|
||||
await page.getByRole("button", { name: "Create room" }).click();
|
||||
|
||||
// Then we created a normal encrypted room, without encrypted state
|
||||
await expect(page.getByText("Encryption enabled")).toBeVisible();
|
||||
await expect(page.getByText("State encryption enabled")).not.toBeVisible();
|
||||
|
||||
// And it has the correct name
|
||||
await expect(page.getByTestId("timeline").getByRole("heading", { name })).toBeVisible();
|
||||
|
||||
// And the room name state event is not encrypted
|
||||
await viewSourceOnRoomNameEvent(page);
|
||||
await expect(page.getByText("Original event source")).toBeVisible();
|
||||
await expect(page.getByText("Decrypted event source")).not.toBeVisible();
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
async function viewSourceOnRoomNameEvent(page: Page) {
|
||||
await page
|
||||
.getByRole("listitem")
|
||||
.filter({ hasText: "created and configured the room" })
|
||||
.getByRole("button", { name: "expand" })
|
||||
.click();
|
||||
|
||||
await page
|
||||
.getByRole("listitem")
|
||||
.filter({ hasText: "changed the room name to" })
|
||||
.getByRole("button", { name: "Options" })
|
||||
.click();
|
||||
|
||||
await page.getByRole("menuitem", { name: "View source" }).click();
|
||||
}
|
||||
|
|
|
|||
174
playwright/e2e/room/room-status-bar.spec.ts
Normal file
|
|
@ -0,0 +1,174 @@
|
|||
/*
|
||||
Copyright 2025 Element Creations Ltd.
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { test, expect } from "../../element-web-test";
|
||||
|
||||
test.describe("Room Status Bar", () => {
|
||||
test.use({
|
||||
displayName: "Jim",
|
||||
page: async ({ page }, use) => {
|
||||
// Increase width as these components look horrible at lower
|
||||
// widths.
|
||||
await page.setViewportSize({ width: 1400, height: 768 });
|
||||
await use(page);
|
||||
},
|
||||
room: async ({ app, user }, use) => {
|
||||
const roomId = await app.client.createRoom({
|
||||
name: "A room",
|
||||
});
|
||||
await app.closeNotificationToast();
|
||||
await app.viewRoomById(roomId);
|
||||
await use({ roomId });
|
||||
},
|
||||
});
|
||||
|
||||
test("should show an error when sync stops", { tag: "@screenshot" }, async ({ page, user, app, room, axe }) => {
|
||||
await page.route("**/_matrix/client/*/sync*", async (route, req) => {
|
||||
await route.fulfill({
|
||||
status: 500,
|
||||
contentType: "application/json",
|
||||
body: '{"error": "Test fail", "errcode": "M_UNKNOWN"}',
|
||||
});
|
||||
});
|
||||
await app.client.sendMessage(room.roomId, "forcing sync to run");
|
||||
const banner = page.getByRole("region", { name: "Room status bar" });
|
||||
await expect(banner).toBeVisible({ timeout: 15000 });
|
||||
await expect(banner).toMatchScreenshot("connectivity_lost.png");
|
||||
});
|
||||
test("should NOT an error when a resource limit is hit", async ({ page, user, app, room, axe, toasts }) => {
|
||||
await app.viewRoomById(room.roomId);
|
||||
await page.route("**/_matrix/client/*/sync*", async (route, req) => {
|
||||
await route.fulfill({
|
||||
status: 400,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
error: "Test fail",
|
||||
errcode: "M_RESOURCE_LIMIT_EXCEEDED",
|
||||
limit_type: "monthly_active_user",
|
||||
admin_contact: "https://example.org",
|
||||
}),
|
||||
});
|
||||
});
|
||||
await app.client.sendMessage(room.roomId, "forcing sync to run");
|
||||
// Wait for the MAU warning toast to appear so we know this status bar would have appeared.
|
||||
await toasts.getToast("Warning", 15000);
|
||||
await expect(page.getByRole("region", { name: "Room status bar" })).not.toBeVisible();
|
||||
});
|
||||
test(
|
||||
"should show an error when the user needs to consent",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, user, app, room, axe }) => {
|
||||
await app.viewRoomById(room.roomId);
|
||||
await page.route("**/_matrix/client/**/send**", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 400,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
error: "Test fail",
|
||||
errcode: "M_CONSENT_NOT_GIVEN",
|
||||
consent_uri: "https://example.org",
|
||||
}),
|
||||
});
|
||||
});
|
||||
const composer = app.getComposerField();
|
||||
await composer.fill("Hello!");
|
||||
await composer.press("Enter");
|
||||
await page
|
||||
.getByRole("dialog", { name: "Terms and Conditions" })
|
||||
.getByRole("button", { name: "Dismiss" })
|
||||
.click();
|
||||
const banner = page.getByRole("region", { name: "Room status bar" });
|
||||
await expect(banner).toBeVisible({ timeout: 15000 });
|
||||
await expect(banner).toMatchScreenshot("consent.png");
|
||||
},
|
||||
);
|
||||
test.describe("Message fails to send", () => {
|
||||
test.beforeEach(async ({ page, user, app, room, axe }) => {
|
||||
await app.viewRoomById(room.roomId);
|
||||
await page.route("**/_matrix/client/**/send**", async (route) => {
|
||||
await route.fulfill({
|
||||
status: 400,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({ error: "Test fail", errcode: "M_UNKNOWN" }),
|
||||
});
|
||||
});
|
||||
const composer = app.getComposerField();
|
||||
await composer.fill("Hello!");
|
||||
await composer.press("Enter");
|
||||
const banner = page.getByRole("region", { name: "Room status bar" });
|
||||
await expect(banner).toBeVisible();
|
||||
});
|
||||
test(
|
||||
"should show an error when a message fails to send",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, user, app, room, axe }) => {
|
||||
const banner = page.getByRole("region", { name: "Room status bar" });
|
||||
await expect(banner).toMatchScreenshot("message_failed.png");
|
||||
},
|
||||
);
|
||||
test("should be able to 'Delete all' messages", async ({ page, user, app, room, axe }) => {
|
||||
const banner = page.getByRole("region", { name: "Room status bar" });
|
||||
await banner.getByRole("button", { name: "Delete all" }).click();
|
||||
await expect(banner).not.toBeVisible();
|
||||
});
|
||||
test("should be able to 'Retry all' messages", async ({ page, user, app, room, axe }) => {
|
||||
const banner = page.getByRole("region", { name: "Room status bar" });
|
||||
await page.unroute("**/_matrix/client/**/send**");
|
||||
await banner.getByRole("button", { name: "Retry all" }).click();
|
||||
await expect(banner).not.toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Local rooms", () => {
|
||||
test.use({
|
||||
botCreateOpts: {
|
||||
displayName: "Alice",
|
||||
},
|
||||
});
|
||||
test(
|
||||
"should show an error when creating a local room fails",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, user, bot }) => {
|
||||
await page
|
||||
.getByRole("navigation", { name: "Room list" })
|
||||
.getByRole("button", { name: "New conversation" })
|
||||
.click();
|
||||
await page.getByRole("menuitem", { name: "Start chat" }).click();
|
||||
|
||||
await page.route("**/_matrix/client/*/createRoom*", async (route, req) => {
|
||||
await route.fulfill({
|
||||
status: 400,
|
||||
contentType: "application/json",
|
||||
body: JSON.stringify({
|
||||
error: "Test fail",
|
||||
errcode: "M_UNKNOWN",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
const other = page.locator(".mx_InviteDialog_other");
|
||||
await other.getByTestId("invite-dialog-input").fill(bot.credentials.userId);
|
||||
await expect(
|
||||
other.getByRole("option", { name: "Alice" }).getByText(bot.credentials.userId),
|
||||
).toBeVisible();
|
||||
await other.getByRole("option", { name: "Alice" }).click();
|
||||
await other.getByRole("button", { name: "Go" }).click();
|
||||
// Send a message to invite the bots
|
||||
const composer = app.getComposerField();
|
||||
await composer.fill("Hello");
|
||||
await composer.press("Enter");
|
||||
|
||||
const banner = page.getByText("!Some of your messages have");
|
||||
await expect(banner).toBeVisible();
|
||||
await expect(banner).toMatchScreenshot("local_room_create_failed.png");
|
||||
|
||||
await page.unroute("**/_matrix/client/*/createRoom*");
|
||||
await banner.getByRole("button", { name: "Retry" }).click();
|
||||
await expect(banner).not.toBeVisible();
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
@ -56,4 +56,35 @@ test.describe("Appearance user settings tab", () => {
|
|||
// Assert that the font-family value was removed
|
||||
await expect(page.locator("body")).toHaveCSS("font-family", '""');
|
||||
});
|
||||
|
||||
test(
|
||||
"should keep same font and emoji when switching theme",
|
||||
{ tag: "@screenshot" },
|
||||
async ({ page, app, user, util }) => {
|
||||
const roomId = await util.createAndDisplayRoom();
|
||||
await app.client.sendMessage(roomId, { body: "Message with 🦡", msgtype: "m.text" });
|
||||
|
||||
await app.settings.openUserSettings("Appearance");
|
||||
const tab = page.getByTestId("mx_AppearanceUserSettingsTab");
|
||||
await tab.getByRole("button", { name: "Show advanced" }).click();
|
||||
await tab.getByRole("switch", { name: "Use bundled emoji font" }).click();
|
||||
await tab.getByRole("switch", { name: "Use a system font" }).click();
|
||||
|
||||
await app.closeDialog();
|
||||
await expect(page).toMatchScreenshot("window-before-switch.png", {
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
});
|
||||
|
||||
// Switch to dark theme
|
||||
await app.settings.openUserSettings("Appearance");
|
||||
await util.getMatchSystemThemeSwitch().click();
|
||||
await util.getDarkTheme().click();
|
||||
|
||||
await app.closeDialog();
|
||||
// Font and emoji should remain the same after theme switch
|
||||
await expect(page).toMatchScreenshot("window-after-switch.png", {
|
||||
mask: [page.locator(".mx_MessageTimestamp")],
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -150,9 +150,10 @@ class Helpers {
|
|||
/**
|
||||
* Create and display a room named Test Room
|
||||
*/
|
||||
async createAndDisplayRoom() {
|
||||
await this.app.client.createRoom({ name: "Test Room" });
|
||||
async createAndDisplayRoom(): Promise<string> {
|
||||
const roomId = await this.app.client.createRoom({ name: "Test Room" });
|
||||
await this.app.viewRoomByName("Test Room");
|
||||
return roomId;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -25,7 +25,9 @@ test.describe("Encryption tab", () => {
|
|||
|
||||
test.beforeEach(async ({ page, homeserver, credentials }) => {
|
||||
// The bot bootstraps cross-signing, creates a key backup and sets up a recovery key
|
||||
const res = await createBot(page, homeserver, credentials);
|
||||
const botCredentials = { ...credentials };
|
||||
delete botCredentials.accessToken; // use a new login for the bot
|
||||
const res = await createBot(page, homeserver, botCredentials);
|
||||
recoveryKey = res.recoveryKey;
|
||||
expectedBackupVersion = res.expectedBackupVersion;
|
||||
});
|
||||
|
|
|
|||
|
|
@ -17,7 +17,9 @@ test.describe("Recovery section in Encryption tab", () => {
|
|||
let recoveryKey: GeneratedSecretStorageKey;
|
||||
test.beforeEach(async ({ page, homeserver, credentials }) => {
|
||||
// The bot bootstraps cross-signing, creates a key backup and sets up a recovery key
|
||||
const res = await createBot(page, homeserver, credentials);
|
||||
const botCredentials = { ...credentials };
|
||||
delete botCredentials.accessToken; // use a new login for the bot
|
||||
const res = await createBot(page, homeserver, botCredentials);
|
||||
recoveryKey = res.recoveryKey;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -74,9 +74,7 @@ test.describe("Roles & Permissions room settings tab", () => {
|
|||
|
||||
await settingsGroupAccess.getByText("Private (invite only)").click();
|
||||
// Element should have automatically set the room to "sharing" history visibility
|
||||
await expect(
|
||||
settingsGroupHistory.getByText("Members only (since the point in time of selecting this option)"),
|
||||
).toBeChecked();
|
||||
await expect(settingsGroupHistory.getByText("Members (full history)")).toBeChecked();
|
||||
},
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import { SettingLevel } from "../../../src/settings/SettingLevel";
|
|||
import { test, expect } from "../../element-web-test";
|
||||
import type { Credentials } from "../../plugins/homeserver";
|
||||
import { Bot } from "../../pages/bot";
|
||||
import { isDendrite } from "../../plugins/homeserver/dendrite";
|
||||
|
||||
// Load a copy of our fake Element Call app, and the latest widget API.
|
||||
// The fake call app does *just* enough to convince Element Web that a call is ongoing
|
||||
|
|
@ -264,6 +265,7 @@ test.describe("Element Call", () => {
|
|||
preset: "trusted_private_chat" as Preset.TrustedPrivateChat,
|
||||
invite: [bot.credentials.userId],
|
||||
});
|
||||
await bot.awaitRoomMembership(roomId);
|
||||
await app.client.setAccountData("m.direct" as EventType.Direct, {
|
||||
[bot.credentials.userId]: [roomId],
|
||||
});
|
||||
|
|
@ -577,4 +579,84 @@ test.describe("Element Call", () => {
|
|||
await openAndJoinCall(page, true);
|
||||
});
|
||||
});
|
||||
|
||||
test.describe("Widget leak bug reproduction", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
test.skip(isDendrite, "No need to test on other HS, this is a client bug reproduction");
|
||||
test.use({
|
||||
config: {
|
||||
features: {
|
||||
feature_video_rooms: true,
|
||||
feature_element_call_video_rooms: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const fakeCallClientSend = readFile("playwright/sample-files/fake-element-call-with-send.html", "utf-8");
|
||||
|
||||
let charlie: Bot;
|
||||
test.use({
|
||||
room: async ({ page, app, user, homeserver, bot }, use) => {
|
||||
charlie = new Bot(page, homeserver, { displayName: "Charlie" });
|
||||
await charlie.prepareClient();
|
||||
const roomId = await app.client.createRoom({
|
||||
name: "VideoRoom",
|
||||
invite: [bot.credentials.userId, charlie.credentials.userId],
|
||||
creation_content: {
|
||||
type: "org.matrix.msc3417.call",
|
||||
},
|
||||
});
|
||||
await app.client.createRoom({
|
||||
name: "OtherRoom",
|
||||
});
|
||||
await use({ roomId });
|
||||
},
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page, user, app }) => {
|
||||
// use a specific widget to reproduce the bug.
|
||||
// Mock a widget page. We use a fake version of Element Call here.
|
||||
// We should match on things after .html as these widgets get a ton of extra params.
|
||||
await page.route(/\/widget-with-send.html.+/, async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
// Do enough to
|
||||
body: (await fakeCallClientSend).replace("widgetCodeHere", await widgetApi),
|
||||
});
|
||||
});
|
||||
await app.settings.setValue(
|
||||
"Developer.elementCallUrl",
|
||||
null,
|
||||
SettingLevel.DEVICE,
|
||||
new URL("/widget-with-send.html#", page.url()).toString(),
|
||||
);
|
||||
});
|
||||
|
||||
test("Switching rooms should not leak widgets", async ({ page, user, room, app }) => {
|
||||
await app.viewRoomByName("VideoRoom");
|
||||
|
||||
await expect(page.getByRole("heading", { name: "Approve widget permissions" })).toBeVisible();
|
||||
// approve
|
||||
await page.getByTestId("dialog-primary-button").click();
|
||||
|
||||
// Switch back and forth a few times to trigger the bug.
|
||||
|
||||
await app.viewRoomByName("OtherRoom");
|
||||
await app.viewRoomByName("VideoRoom");
|
||||
await app.viewRoomByName("OtherRoom");
|
||||
await app.viewRoomByName("VideoRoom");
|
||||
|
||||
// For this test we want to display the chat area alongside the widget
|
||||
await page.getByRole("button", { name: "Chat" }).click();
|
||||
|
||||
await page
|
||||
.locator('iframe[title="Element Call"]')
|
||||
.contentFrame()
|
||||
.getByRole("button", { name: "Send Room Message" })
|
||||
.click();
|
||||
|
||||
const messageSent = await page.getByText("I sent this once!!").count();
|
||||
|
||||
expect(messageSent).toBe(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ test.describe("PSTN", () => {
|
|||
await toasts.rejectToast("Notifications");
|
||||
await toasts.assertNoToasts();
|
||||
|
||||
await expect(page.locator(".mx_RoomListSearch")).toMatchScreenshot("dialpad-trigger.png");
|
||||
await expect(page.getByTestId("room-list-search")).toMatchScreenshot("dialpad-trigger.png");
|
||||
await page.getByLabel("Open dial pad").click();
|
||||
await expect(page.locator(".mx_Dialog")).toMatchScreenshot("dialpad.png");
|
||||
});
|
||||
|
|
|
|||
|
|
@ -244,24 +244,6 @@ export class ElementAppPage {
|
|||
await this.page.getByRole("dialog").getByRole("button", { name: "Invite" }).click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a locator for the tooltip associated with an element
|
||||
* @param e The element with the tooltip
|
||||
* @returns Locator to the tooltip
|
||||
*/
|
||||
public async getTooltipForElement(e: Locator): Promise<Locator> {
|
||||
const [labelledById, describedById] = await Promise.all([
|
||||
e.getAttribute("aria-labelledby"),
|
||||
e.getAttribute("aria-describedby"),
|
||||
]);
|
||||
if (!labelledById && !describedById) {
|
||||
throw new Error(
|
||||
"Element has no aria-labelledby or aria-describedy attributes! The tooltip should have added either one of these.",
|
||||
);
|
||||
}
|
||||
return this.page.locator(`id=${labelledById ?? describedById}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the notification toast
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -16,6 +16,10 @@ import type { Credentials, HomeserverInstance } from "../plugins/homeserver";
|
|||
import type { GeneratedSecretStorageKey } from "matrix-js-sdk/src/crypto-api";
|
||||
import { bootstrapCrossSigningForClient, Client } from "./client";
|
||||
|
||||
export interface CredentialsOptionalAccessToken extends Omit<Credentials, "accessToken"> {
|
||||
accessToken?: string;
|
||||
}
|
||||
|
||||
export interface CreateBotOpts {
|
||||
/**
|
||||
* A prefix to use for the userid. If unspecified, "bot_" will be used.
|
||||
|
|
@ -58,7 +62,7 @@ const defaultCreateBotOptions = {
|
|||
type ExtendedMatrixClient = MatrixClient & { __playwright_recovery_key: GeneratedSecretStorageKey };
|
||||
|
||||
export class Bot extends Client {
|
||||
public credentials?: Credentials;
|
||||
public credentials?: CredentialsOptionalAccessToken;
|
||||
private handlePromise: Promise<JSHandle<ExtendedMatrixClient>>;
|
||||
|
||||
constructor(
|
||||
|
|
@ -70,7 +74,16 @@ export class Bot extends Client {
|
|||
this.opts = Object.assign({}, defaultCreateBotOptions, opts);
|
||||
}
|
||||
|
||||
public setCredentials(credentials: Credentials): void {
|
||||
/**
|
||||
* Set the credentials used by the bot.
|
||||
*
|
||||
* If `credentials.accessToken` is unset, then `buildClient` will log in a
|
||||
* new session. Note that `getCredentials` will return the credentials
|
||||
* passed to this function, rather than the updated credentials from the new
|
||||
* login. In particular, the `accessToken` and `deviceId` will not be
|
||||
* updated.
|
||||
*/
|
||||
public setCredentials(credentials: CredentialsOptionalAccessToken): void {
|
||||
if (this.credentials) throw new Error("Bot has already started");
|
||||
this.credentials = credentials;
|
||||
}
|
||||
|
|
@ -80,7 +93,7 @@ export class Bot extends Client {
|
|||
return client.evaluate((cli) => cli.__playwright_recovery_key);
|
||||
}
|
||||
|
||||
private async getCredentials(): Promise<Credentials> {
|
||||
private async getCredentials(): Promise<CredentialsOptionalAccessToken> {
|
||||
if (this.credentials) return this.credentials;
|
||||
// We want to pad the uniqueId but not the prefix
|
||||
const username =
|
||||
|
|
@ -161,6 +174,30 @@ export class Bot extends Client {
|
|||
getSecretStorageKey,
|
||||
};
|
||||
|
||||
if (!("accessToken" in credentials)) {
|
||||
const loginCli = new window.matrixcs.MatrixClient({
|
||||
baseUrl,
|
||||
store: new window.matrixcs.MemoryStore(),
|
||||
scheduler: new window.matrixcs.MatrixScheduler(),
|
||||
cryptoStore: new window.matrixcs.MemoryCryptoStore(),
|
||||
cryptoCallbacks,
|
||||
logger,
|
||||
});
|
||||
|
||||
const loginResponse = await loginCli.loginRequest({
|
||||
type: "m.login.password",
|
||||
identifier: {
|
||||
type: "m.id.user",
|
||||
user: credentials.userId,
|
||||
},
|
||||
password: credentials.password,
|
||||
});
|
||||
|
||||
credentials.accessToken = loginResponse.access_token;
|
||||
credentials.userId = loginResponse.user_id;
|
||||
credentials.deviceId = loginResponse.device_id;
|
||||
}
|
||||
|
||||
const cli = new window.matrixcs.MatrixClient({
|
||||
baseUrl,
|
||||
userId: credentials.userId,
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import type {
|
|||
EmptyObject,
|
||||
} from "matrix-js-sdk/src/matrix";
|
||||
import type { RoomMessageEventContent } from "matrix-js-sdk/src/types";
|
||||
import { type Credentials } from "../plugins/homeserver";
|
||||
import { type CredentialsOptionalAccessToken } from "./bot";
|
||||
|
||||
export class Client {
|
||||
public network: Network;
|
||||
|
|
@ -424,7 +424,7 @@ export class Client {
|
|||
/**
|
||||
* Bootstraps cross-signing.
|
||||
*/
|
||||
public async bootstrapCrossSigning(credentials: Credentials): Promise<void> {
|
||||
public async bootstrapCrossSigning(credentials: CredentialsOptionalAccessToken): Promise<void> {
|
||||
const client = await this.prepareClient();
|
||||
return bootstrapCrossSigningForClient(client, credentials);
|
||||
}
|
||||
|
|
@ -522,7 +522,7 @@ export class Client {
|
|||
*/
|
||||
export function bootstrapCrossSigningForClient(
|
||||
client: JSHandle<MatrixClient>,
|
||||
credentials: Credentials,
|
||||
credentials: CredentialsOptionalAccessToken,
|
||||
resetKeys: boolean = false,
|
||||
) {
|
||||
return client.evaluate(
|
||||
|
|
|
|||
|
|
@ -15,11 +15,12 @@ export class Toasts {
|
|||
* Assert that a toast with the given title exists, and return it
|
||||
*
|
||||
* @param expectedTitle - Expected title of the toast
|
||||
* @param timeout Time to retry the assertion for in milliseconds. Defaults to `timeout` in `TestConfig.expect`.
|
||||
* @returns the Locator for the matching toast
|
||||
*/
|
||||
public async getToast(expectedTitle: string): Promise<Locator> {
|
||||
public async getToast(expectedTitle: string, timeout?: number): Promise<Locator> {
|
||||
const toast = this.page.locator(".mx_Toast_toast", { hasText: expectedTitle }).first();
|
||||
await expect(toast).toBeVisible();
|
||||
await expect(toast).toBeVisible({ timeout });
|
||||
return toast;
|
||||
}
|
||||
|
||||
|
|
|
|||
53
playwright/sample-files/fake-element-call-with-send.html
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
<!doctype html>
|
||||
<style>
|
||||
body {
|
||||
background: rgb(139, 192, 253);
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- element-call.spec.ts will insert the widget API in this block -->
|
||||
<script>
|
||||
widgetCodeHere;
|
||||
</script>
|
||||
|
||||
<div>
|
||||
<p>Fake Element Call</p>
|
||||
<p>State: <span id="state">Loading</span></p>
|
||||
<button id="send-button">Send Room Message</button>
|
||||
</div>
|
||||
|
||||
<!-- Minimal fake implementation of Element Call. Just enough for testing the leagkin widgets.-->
|
||||
<script>
|
||||
const stateIndicator = document.querySelector("#state");
|
||||
const { WidgetApi, WidgetApiToWidgetAction, MatrixCapabilities } = mxwidgets();
|
||||
const widgetId = new URLSearchParams(window.location.search).get("widgetId");
|
||||
const params = new URLSearchParams(window.location.hash.slice(1));
|
||||
|
||||
const roomId = params.get("roomId");
|
||||
const api = new WidgetApi(widgetId, "*");
|
||||
|
||||
document.querySelector("#send-button").onclick = async () => {
|
||||
await api.sendRoomEvent(
|
||||
"m.room.message",
|
||||
{ msgtype: "m.text", body: "I sent this once!!" },
|
||||
roomId,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
);
|
||||
};
|
||||
|
||||
api.requestCapability(MatrixCapabilities.AlwaysOnScreen);
|
||||
api.requestCapability(`org.matrix.msc2762.timeline:${roomId}`);
|
||||
api.requestCapabilityToSendMessage("m.text");
|
||||
|
||||
api.on("ready", (ev) => {
|
||||
stateIndicator.innerHTML = "Ready";
|
||||
});
|
||||
|
||||
// Start the messaging
|
||||
api.start();
|
||||
|
||||
// If waitForIframeLoad is false, tell the client that we're good to go
|
||||
api.sendContentLoaded();
|
||||
</script>
|
||||
|
Before Width: | Height: | Size: 227 KiB After Width: | Height: | Size: 204 KiB |
|
Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 8.5 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 17 KiB |
|
After Width: | Height: | Size: 24 KiB |
|
After Width: | Height: | Size: 21 KiB |
|
After Width: | Height: | Size: 441 B |
|
After Width: | Height: | Size: 393 B |
|
After Width: | Height: | Size: 393 B |
|
After Width: | Height: | Size: 372 B |
|
Before Width: | Height: | Size: 53 KiB After Width: | Height: | Size: 53 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
|
Before Width: | Height: | Size: 71 KiB After Width: | Height: | Size: 71 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 91 KiB |
|
Before Width: | Height: | Size: 68 KiB After Width: | Height: | Size: 69 KiB |
|
Before Width: | Height: | Size: 984 KiB After Width: | Height: | Size: 984 KiB |
|
Before Width: | Height: | Size: 1 MiB After Width: | Height: | Size: 1 MiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 77 KiB |
|
Before Width: | Height: | Size: 72 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 35 KiB After Width: | Height: | Size: 35 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 33 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 25 KiB |
|
After Width: | Height: | Size: 8.6 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 5.8 KiB |
|
After Width: | Height: | Size: 11 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
After Width: | Height: | Size: 74 KiB |
|
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 63 KiB |
|
Before Width: | Height: | Size: 87 KiB After Width: | Height: | Size: 82 KiB |
|
Before Width: | Height: | Size: 96 KiB After Width: | Height: | Size: 90 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 4.7 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |