Compare commits
85 Commits
rav/reenab
...
t3chguy/mo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01ffd72b9d | ||
|
|
0eee4e3a18 | ||
|
|
98ed46b0f3 | ||
|
|
78690f032d | ||
|
|
607dbb03df | ||
|
|
bdfdf5fc49 | ||
|
|
cc094f4b56 | ||
|
|
2d0facd47b | ||
|
|
c53b17d291 | ||
|
|
8086262e04 | ||
|
|
f9a0a626a6 | ||
|
|
d7f54355ac | ||
|
|
a668216e20 | ||
|
|
1cadf1a82e | ||
|
|
ee37734cfc | ||
|
|
15f1291cbc | ||
|
|
8a550cf3f6 | ||
|
|
9c911d5c59 | ||
|
|
6fca4d106e | ||
|
|
24f923feac | ||
|
|
9be2b973d0 | ||
|
|
d837d2f62d | ||
|
|
f2379878cd | ||
|
|
261d073f6d | ||
|
|
401fc63eb0 | ||
|
|
51c4506431 | ||
|
|
1de27b265b | ||
|
|
db9514760d | ||
|
|
4b8f404bb3 | ||
|
|
e10b1f9222 | ||
|
|
ff87df4825 | ||
|
|
c1a163cbc9 | ||
|
|
1e6f9dd096 | ||
|
|
6a8493c6eb | ||
|
|
12927cc4a7 | ||
|
|
814f4a85df | ||
|
|
475504d33b | ||
|
|
7faee3d1b7 | ||
|
|
30e7567064 | ||
|
|
2250f5e6a2 | ||
|
|
e43b696461 | ||
|
|
bf98ede4fa | ||
|
|
cc0ece9837 | ||
|
|
ab6ef2fa85 | ||
|
|
c79c8c836b | ||
|
|
3f0dcaa64c | ||
|
|
652e891663 | ||
|
|
7eb5a29cf0 | ||
|
|
1b38624fd8 | ||
|
|
d98533025a | ||
|
|
c3e5367e45 | ||
|
|
1e15a322a5 | ||
|
|
452996eacf | ||
|
|
ee120f2fa9 | ||
|
|
94aa51dc57 | ||
|
|
e19d3dcd44 | ||
|
|
5a4b5418cc | ||
|
|
d1f62317ba | ||
|
|
9232a220dc | ||
|
|
07e6f0af35 | ||
|
|
be318f9cb8 | ||
|
|
5ff42e8548 | ||
|
|
45a2fd9d63 | ||
|
|
7e40e3697f | ||
|
|
6599601000 | ||
|
|
beaabd5b44 | ||
|
|
db5c69e228 | ||
|
|
a23a2c03d3 | ||
|
|
c2c040dd42 | ||
|
|
c98358cb26 | ||
|
|
d384a9b71b | ||
|
|
fc04ad26ce | ||
|
|
6fb9098c30 | ||
|
|
9e50e59168 | ||
|
|
b5160c47b3 | ||
|
|
3af8273d6b | ||
|
|
008b004976 | ||
|
|
68a2748813 | ||
|
|
acb3d31a07 | ||
|
|
9136332f42 | ||
|
|
61e266e8e5 | ||
|
|
a7c7c27ae8 | ||
|
|
1414b8ff54 | ||
|
|
c6a5d288ed | ||
|
|
8c2a035302 |
19
.github/workflows/docker.yaml
vendored
@@ -37,14 +37,14 @@ jobs:
|
||||
install: true
|
||||
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to GitHub Container Registry
|
||||
uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3
|
||||
uses: docker/login-action@184bdaa0721073962dff0199f1fb9940f07167d1 # v3
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
registry: ghcr.io
|
||||
@@ -96,7 +96,7 @@ jobs:
|
||||
|
||||
- name: Docker meta
|
||||
id: meta
|
||||
uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5
|
||||
uses: docker/metadata-action@c1e51972afc2121e065aed6d45c65596fe445f3f # v5
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
images: |
|
||||
@@ -139,3 +139,16 @@ jobs:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
repository: vectorim/element-web
|
||||
|
||||
- name: Repository Dispatch
|
||||
uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3
|
||||
if: github.event_name != 'pull_request'
|
||||
with:
|
||||
repository: element-hq/element-web-pro
|
||||
token: ${{ secrets.ELEMENT_BOT_TOKEN }}
|
||||
event-type: image-built
|
||||
# Stable way to determine the :version
|
||||
client-payload: |-
|
||||
{
|
||||
"base-ref": "${{ fromJSON(steps.meta.outputs.json).labels['org.opencontainers.image.version'] }}"
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import "../res/css/shared.pcss";
|
||||
import "./preview.css";
|
||||
import React, { useLayoutEffect } from "react";
|
||||
import { FORCE_RE_RENDER } from "storybook/internal/core-events";
|
||||
import { setLanguage } from "../src/shared-components/i18n";
|
||||
import { setLanguage } from "../src/shared-components/utils/i18n";
|
||||
import { TooltipProvider } from "@vector-im/compound-web";
|
||||
|
||||
export const globalTypes = {
|
||||
|
||||
@@ -70,5 +70,13 @@ module.exports = {
|
||||
],
|
||||
},
|
||||
],
|
||||
"property-no-deprecated": [
|
||||
true,
|
||||
{
|
||||
ignoreProperties: ["-webkit-box-orient", "word-wrap"],
|
||||
},
|
||||
],
|
||||
"nesting-selector-no-missing-scoping-root": null,
|
||||
"no-invalid-position-declaration": null,
|
||||
},
|
||||
};
|
||||
|
||||
26
CHANGELOG.md
@@ -1,3 +1,29 @@
|
||||
Changes in [1.11.108](https://github.com/element-hq/element-web/releases/tag/v1.11.108) (2025-07-30)
|
||||
====================================================================================================
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* [Backport staging] Fix downloaded attachments not being decrypted ([#30434](https://github.com/element-hq/element-web/pull/30434)). Contributed by @RiotRobot.
|
||||
|
||||
|
||||
Changes in [1.11.107](https://github.com/element-hq/element-web/releases/tag/v1.11.107) (2025-07-29)
|
||||
====================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
* Message preview should show tooltip with the full message on hover ([#30265](https://github.com/element-hq/element-web/pull/30265)). Contributed by @MidhunSureshR.
|
||||
* Support rendering notification badges on platforms that do their own icon overlays ([#30315](https://github.com/element-hq/element-web/pull/30315)). Contributed by @Half-Shot.
|
||||
* Add SubscriptionViewModel base class ([#30297](https://github.com/element-hq/element-web/pull/30297)). Contributed by @dbkr.
|
||||
* Enhancement: Save image on CTRL+S ([#30330](https://github.com/element-hq/element-web/pull/30330)). Contributed by @ioalexander.
|
||||
* Add quote functionality to MessageContextMenu (#29893) ([#30323](https://github.com/element-hq/element-web/pull/30323)). Contributed by @AlirezaMrtz.
|
||||
* Initial structure for shared component views ([#30216](https://github.com/element-hq/element-web/pull/30216)). Contributed by @dbkr.
|
||||
|
||||
## 🐛 Bug Fixes
|
||||
|
||||
* [Backport staging] Fix e2e shield being invisible in white mode for encrypted room ([#30411](https://github.com/element-hq/element-web/pull/30411)). Contributed by @RiotRobot.
|
||||
* Force ED titlebar color for new room list ([#30332](https://github.com/element-hq/element-web/pull/30332)). Contributed by @florianduros.
|
||||
* Add a background color to left panel for macos titlebar in element desktop ([#30328](https://github.com/element-hq/element-web/pull/30328)). Contributed by @florianduros.
|
||||
* Fix: Prevent page refresh on Enter key in right panel member search ([#30312](https://github.com/element-hq/element-web/pull/30312)). Contributed by @AlirezaMrtz.
|
||||
|
||||
|
||||
Changes in [1.11.106](https://github.com/element-hq/element-web/releases/tag/v1.11.106) (2025-07-15)
|
||||
====================================================================================================
|
||||
## ✨ Features
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# syntax=docker.io/docker/dockerfile:1.17-labs@sha256:9187104f31e3a002a8a6a3209ea1f937fb7486c093cbbde1e14b0fa0d7e4f1b5
|
||||
|
||||
# Builder
|
||||
FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:a80324457a2c8d09c83ff9edf2bdf71f378d3288de920e68a358bd3c484b8c4a AS builder
|
||||
FROM --platform=$BUILDPLATFORM node:22-bullseye@sha256:2d63e0f812d023c4c764e83d7e30dc94949304443ebc372d5c445e63a5ae49c1 AS builder
|
||||
|
||||
# Support custom branch of the js-sdk. This also helps us build images of element-web develop.
|
||||
ARG USE_CUSTOM_SDKS=false
|
||||
@@ -19,7 +19,7 @@ RUN /src/scripts/docker-package.sh
|
||||
RUN cp /src/config.sample.json /src/webapp/config.json
|
||||
|
||||
# App
|
||||
FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:86df552d36eb24c45d3f5becf6423bd056a3fd235d7085fe3d5ea28ba89a8232
|
||||
FROM nginxinc/nginx-unprivileged:alpine-slim@sha256:e61b77b27c8f3124fad6d19e894ca5b603bcaf6a34a2df035511299dfa6fad35
|
||||
|
||||
# Need root user to install packages & manipulate the usr directory
|
||||
USER root
|
||||
|
||||
@@ -40,8 +40,6 @@ const config: Config = {
|
||||
"^!!raw-loader!.*": "jest-raw-loader",
|
||||
"recorderWorkletFactory": "<rootDir>/__mocks__/empty.js",
|
||||
"^fetch-mock$": "<rootDir>/node_modules/fetch-mock",
|
||||
// Requires ESM which is incompatible with our current Jest setup
|
||||
"^@element-hq/element-web-module-api$": "<rootDir>/__mocks__/empty.js",
|
||||
},
|
||||
transformIgnorePatterns: ["/node_modules/(?!(mime|matrix-js-sdk)).+$"],
|
||||
collectCoverageFrom: [
|
||||
|
||||
39
package.json
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "element-web",
|
||||
"version": "1.11.106",
|
||||
"version": "1.11.108",
|
||||
"description": "Element: the future of secure communication",
|
||||
"author": "New Vector Ltd.",
|
||||
"repository": {
|
||||
@@ -73,10 +73,10 @@
|
||||
"test:storybook:update": "playwright-screenshots --entrypoint yarn --with-node-modules && playwright-screenshots --entrypoint /work/node_modules/.bin/test-storybook --with-node-modules --url http://host.docker.internal:6007/ --updateSnapshot"
|
||||
},
|
||||
"resolutions": {
|
||||
"**/pretty-format/react-is": "19.1.0",
|
||||
"@playwright/test": "1.54.1",
|
||||
"@types/react": "19.1.8",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"**/pretty-format/react-is": "19.1.1",
|
||||
"@playwright/test": "1.54.2",
|
||||
"@types/react": "19.1.9",
|
||||
"@types/react-dom": "19.1.7",
|
||||
"oidc-client-ts": "3.3.0",
|
||||
"jwt-decode": "4.0.0",
|
||||
"caniuse-lite": "1.0.30001724",
|
||||
@@ -86,7 +86,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@element-hq/element-web-module-api": "1.3.0",
|
||||
"@element-hq/element-web-module-api": "1.4.1",
|
||||
"@fontsource/inconsolata": "^5",
|
||||
"@fontsource/inter": "^5",
|
||||
"@formatjs/intl-segmenter": "^11.5.7",
|
||||
@@ -94,12 +94,12 @@
|
||||
"@matrix-org/emojibase-bindings": "^1.3.4",
|
||||
"@matrix-org/react-sdk-module-api": "^2.4.0",
|
||||
"@matrix-org/spec": "^1.7.0",
|
||||
"@sentry/browser": "^9.0.0",
|
||||
"@sentry/browser": "^10.0.0",
|
||||
"@types/png-chunks-extract": "^1.0.2",
|
||||
"@types/react-virtualized": "^9.21.30",
|
||||
"@vector-im/compound-design-tokens": "^5.0.0",
|
||||
"@vector-im/compound-design-tokens": "^6.0.0",
|
||||
"@vector-im/compound-web": "^8.1.2",
|
||||
"@vector-im/matrix-wysiwyg": "2.38.4",
|
||||
"@vector-im/matrix-wysiwyg": "2.39.0",
|
||||
"@zxcvbn-ts/core": "^3.0.4",
|
||||
"@zxcvbn-ts/language-common": "^3.0.4",
|
||||
"@zxcvbn-ts/language-en": "^3.0.2",
|
||||
@@ -117,7 +117,7 @@
|
||||
"emojibase-regex": "15.3.2",
|
||||
"escape-html": "^1.0.3",
|
||||
"file-saver": "^2.0.5",
|
||||
"filesize": "10.1.6",
|
||||
"filesize": "11.0.2",
|
||||
"github-markdown-css": "^5.5.1",
|
||||
"glob-to-regexp": "^0.4.1",
|
||||
"highlight.js": "^11.3.1",
|
||||
@@ -128,9 +128,9 @@
|
||||
"jsrsasign": "^11.0.0",
|
||||
"jszip": "^3.7.0",
|
||||
"katex": "^0.16.0",
|
||||
"linkify-react": "4.3.1",
|
||||
"linkify-string": "4.3.1",
|
||||
"linkifyjs": "4.3.1",
|
||||
"linkify-react": "4.3.2",
|
||||
"linkify-string": "4.3.2",
|
||||
"linkifyjs": "4.3.2",
|
||||
"lodash": "^4.17.21",
|
||||
"maplibre-gl": "^5.0.0",
|
||||
"matrix-encrypt-attachment": "^1.0.3",
|
||||
@@ -154,6 +154,7 @@
|
||||
"react-string-replace": "^1.1.1",
|
||||
"react-transition-group": "^4.4.1",
|
||||
"react-virtualized": "^9.22.5",
|
||||
"react-virtuoso": "^4.12.6",
|
||||
"rfc4648": "^1.4.0",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"sanitize-html": "2.17.0",
|
||||
@@ -185,8 +186,8 @@
|
||||
"@babel/preset-typescript": "^7.12.7",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
"@casualbot/jest-sonar-reporter": "2.2.7",
|
||||
"@element-hq/element-call-embedded": "0.13.1",
|
||||
"@element-hq/element-web-playwright-common": "^1.4.3",
|
||||
"@element-hq/element-call-embedded": "0.14.1",
|
||||
"@element-hq/element-web-playwright-common": "^1.4.4",
|
||||
"@peculiar/webcrypto": "^1.4.3",
|
||||
"@playwright/test": "^1.50.1",
|
||||
"@principalstudio/html-webpack-inject-preload": "^1.2.7",
|
||||
@@ -222,9 +223,9 @@
|
||||
"@types/node-fetch": "^2.6.2",
|
||||
"@types/pako": "^2.0.0",
|
||||
"@types/qrcode": "^1.3.5",
|
||||
"@types/react": "19.1.8",
|
||||
"@types/react": "19.1.9",
|
||||
"@types/react-beautiful-dnd": "^13.0.0",
|
||||
"@types/react-dom": "19.1.6",
|
||||
"@types/react-dom": "19.1.7",
|
||||
"@types/react-transition-group": "^4.4.0",
|
||||
"@types/sanitize-html": "2.16.0",
|
||||
"@types/semver": "^7.5.8",
|
||||
@@ -299,8 +300,8 @@
|
||||
"semver": "^7.5.2",
|
||||
"source-map-loader": "^5.0.0",
|
||||
"storybook": "^9.0.12",
|
||||
"stylelint": "^16.13.0",
|
||||
"stylelint-config-standard": "^38.0.0",
|
||||
"stylelint": "^16.23.0",
|
||||
"stylelint-config-standard": "^39.0.0",
|
||||
"stylelint-scss": "^6.0.0",
|
||||
"stylelint-value-no-unknown-custom-properties": "^6.0.1",
|
||||
"terser-webpack-plugin": "^5.3.9",
|
||||
|
||||
@@ -19,6 +19,7 @@ const clickButtonReply = async (tile: Locator) => {
|
||||
await tile.hover();
|
||||
await tile.getByRole("button", { name: "Reply", exact: true }).click();
|
||||
}).toPass();
|
||||
await expect(tile.page().getByText("Replying", { exact: true })).toBeVisible();
|
||||
};
|
||||
|
||||
test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
@@ -39,7 +40,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
// wait for the tile to finish loading
|
||||
await expect(
|
||||
page
|
||||
.locator(".mx_AudioPlayer_mediaName")
|
||||
.getByTestId("audio-player-name")
|
||||
.last()
|
||||
.filter({ hasText: file.split("/").at(-1) }),
|
||||
).toBeVisible();
|
||||
@@ -54,12 +55,10 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
// Check that the audio player is rendered and its button becomes visible
|
||||
const checkPlayerVisibility = async (locator: Locator) => {
|
||||
// Assert that the audio player and media information are visible
|
||||
const mediaInfo = locator.locator(
|
||||
".mx_EventTile_mediaLine .mx_MAudioBody .mx_AudioPlayer_container .mx_AudioPlayer_mediaInfo",
|
||||
);
|
||||
await expect(mediaInfo.locator(".mx_AudioPlayer_mediaName", { hasText: ".ogg" })).toBeVisible(); // extension
|
||||
await expect(mediaInfo.locator(".mx_AudioPlayer_byline", { hasText: "00:01" })).toBeVisible();
|
||||
await expect(mediaInfo.locator(".mx_AudioPlayer_byline", { hasText: "(3.56 KB)" })).toBeVisible(); // actual size
|
||||
const mediaInfo = locator.getByRole("region", { name: "Audio player" });
|
||||
await expect(mediaInfo.getByText(".ogg")).toBeVisible(); // extension
|
||||
await expect(mediaInfo.getByRole("time")).toHaveText("00:01"); // duration
|
||||
await expect(mediaInfo.getByText("(3.56 KB)")).toBeVisible(); // actual size;
|
||||
|
||||
// Assert that the play button can be found and is visible
|
||||
await expect(locator.getByRole("button", { name: "Play" })).toBeVisible();
|
||||
@@ -78,7 +77,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
}
|
||||
|
||||
// Check the status of the seek bar
|
||||
expect(await page.locator(".mx_AudioPlayer_seek input[type='range']").count()).toBeGreaterThan(0);
|
||||
expect(await page.getByRole("region", { name: "Audio player" }).getByRole("slider").count()).toBeGreaterThan(0);
|
||||
|
||||
// Enable IRC layout
|
||||
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.IRC);
|
||||
@@ -100,7 +99,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
display: none !important;
|
||||
}
|
||||
`,
|
||||
mask: [page.locator(".mx_AudioPlayer_seek")],
|
||||
mask: [page.getByTestId("audio-player-seek")],
|
||||
};
|
||||
|
||||
// Take a snapshot of mx_EventTile_last on IRC layout
|
||||
@@ -186,9 +185,9 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
await uploadFile(page, "playwright/sample-files/1sec.ogg");
|
||||
|
||||
// Assert that the audio player is rendered
|
||||
const container = page.locator(".mx_EventTile_last .mx_AudioPlayer_container");
|
||||
const container = page.locator(".mx_EventTile_last").getByRole("region", { name: "Audio player" });
|
||||
// Assert that the counter is zero before clicking the play button
|
||||
await expect(container.locator(".mx_AudioPlayer_seek [role='timer']", { hasText: "00:00" })).toBeVisible();
|
||||
await expect(container.getByRole("timer")).toHaveText("00:00");
|
||||
|
||||
// Find and click "Play" button, the wait is to make the test less flaky
|
||||
await expect(container.getByRole("button", { name: "Play" })).toBeVisible();
|
||||
@@ -198,7 +197,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
await expect(container.getByRole("button", { name: "Pause" })).toBeVisible();
|
||||
|
||||
// Assert that the timer is reset when the audio file finished playing
|
||||
await expect(container.locator(".mx_AudioPlayer_seek [role='timer']", { hasText: "00:00" })).toBeVisible();
|
||||
await expect(container.getByRole("timer")).toHaveText("00:00");
|
||||
|
||||
// Assert that "Play" button can be found
|
||||
await expect(container.getByRole("button", { name: "Play" })).toBeVisible();
|
||||
@@ -226,7 +225,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
await uploadFile(page, "playwright/sample-files/1sec.ogg");
|
||||
|
||||
// Assert the audio player is rendered
|
||||
await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
|
||||
await expect(page.getByRole("region", { name: "Audio player" })).toBeVisible();
|
||||
|
||||
// Find and click "Reply" button on MessageActionBar
|
||||
const tile = page.locator(".mx_EventTile_last");
|
||||
@@ -236,7 +235,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
await uploadFile(page, "playwright/sample-files/1sec.ogg");
|
||||
|
||||
// Assert that the audio player is rendered
|
||||
await expect(tile.locator(".mx_AudioPlayer_container")).toBeVisible();
|
||||
await expect(tile.getByRole("region", { name: "Audio player" })).toBeVisible();
|
||||
|
||||
// Assert that replied audio file is rendered as file button inside ReplyChain
|
||||
const button = tile.locator(".mx_ReplyChain_wrapper .mx_MFileBody_info[role='button']");
|
||||
@@ -261,7 +260,9 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
await uploadFile(page, "playwright/sample-files/upload-first.ogg");
|
||||
|
||||
// Assert that the audio player is rendered
|
||||
await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
|
||||
await expect(
|
||||
page.locator(".mx_EventTile_last").getByRole("region", { name: "Audio player" }),
|
||||
).toBeVisible();
|
||||
|
||||
await clickButtonReply(tile);
|
||||
|
||||
@@ -269,7 +270,9 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
await uploadFile(page, "playwright/sample-files/upload-second.ogg");
|
||||
|
||||
// Assert that the audio player is rendered
|
||||
await expect(page.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
|
||||
await expect(
|
||||
page.locator(".mx_EventTile_last").getByRole("region", { name: "Audio player" }),
|
||||
).toBeVisible();
|
||||
|
||||
await clickButtonReply(tile);
|
||||
|
||||
@@ -277,7 +280,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
await uploadFile(page, "playwright/sample-files/upload-third.ogg");
|
||||
|
||||
// Assert that the audio player is rendered
|
||||
await expect(tile.locator(".mx_AudioPlayer_container")).toBeVisible();
|
||||
await expect(tile.getByRole("region", { name: "Audio player" })).toBeVisible();
|
||||
|
||||
// Assert that there are two "mx_ReplyChain" elements
|
||||
await expect(tile.locator(".mx_ReplyChain")).toHaveCount(2);
|
||||
@@ -313,7 +316,9 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
// On the main timeline
|
||||
const messageList = page.locator(".mx_RoomView_MessageList");
|
||||
// Assert the audio player is rendered
|
||||
await expect(messageList.locator(".mx_EventTile_last .mx_AudioPlayer_container")).toBeVisible();
|
||||
await expect(
|
||||
messageList.locator(".mx_EventTile_last").getByRole("region", { name: "Audio player" }),
|
||||
).toBeVisible();
|
||||
// Find and click "Reply in thread" button
|
||||
await messageList.locator(".mx_EventTile_last").hover();
|
||||
await messageList.locator(".mx_EventTile_last").getByRole("button", { name: "Reply in thread" }).click();
|
||||
@@ -321,10 +326,10 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
// On a thread
|
||||
const thread = page.locator(".mx_ThreadView");
|
||||
const threadTile = thread.locator(".mx_EventTile_last");
|
||||
const audioPlayer = threadTile.locator(".mx_AudioPlayer_container");
|
||||
const audioPlayer = threadTile.getByRole("region", { name: "Audio player" });
|
||||
|
||||
// Assert that the counter is zero before clicking the play button
|
||||
await expect(audioPlayer.locator(".mx_AudioPlayer_seek [role='timer']", { hasText: "00:00" })).toBeVisible();
|
||||
await expect(audioPlayer.getByRole("timer")).toHaveText("00:00");
|
||||
|
||||
// Find and click "Play" button, the wait is to make the test less flaky
|
||||
await expect(audioPlayer.getByRole("button", { name: "Play" })).toBeVisible();
|
||||
@@ -334,7 +339,7 @@ test.describe("Audio player", { tag: ["@no-firefox", "@no-webkit"] }, () => {
|
||||
await expect(audioPlayer.getByRole("button", { name: "Pause" })).toBeVisible();
|
||||
|
||||
// Assert that the timer is reset when the audio file finished playing
|
||||
await expect(audioPlayer.locator(".mx_AudioPlayer_seek [role='timer']", { hasText: "00:00" })).toBeVisible();
|
||||
await expect(audioPlayer.getByRole("timer")).toHaveText("00:00");
|
||||
|
||||
// Assert that "Play" button can be found
|
||||
await expect(audioPlayer.getByRole("button", { name: "Play" })).not.toBeDisabled();
|
||||
|
||||
@@ -28,7 +28,7 @@ test.describe("Composer", () => {
|
||||
|
||||
test.describe("CIDER", () => {
|
||||
test("sends a message when you click send or press Enter", async ({ page }) => {
|
||||
const composer = page.getByRole("textbox", { name: "Send a message…" });
|
||||
const composer = page.getByRole("textbox", { name: "Send an unencrypted message…" });
|
||||
|
||||
// Type a message
|
||||
await composer.pressSequentially("my message 0");
|
||||
@@ -52,7 +52,7 @@ test.describe("Composer", () => {
|
||||
});
|
||||
|
||||
test("can write formatted text", async ({ page }) => {
|
||||
const composer = page.getByRole("textbox", { name: "Send a message…" });
|
||||
const composer = page.getByRole("textbox", { name: "Send an unencrypted message…" });
|
||||
|
||||
await composer.pressSequentially("my bold");
|
||||
await composer.press(`${CtrlOrMeta}+KeyB`);
|
||||
@@ -68,7 +68,7 @@ test.describe("Composer", () => {
|
||||
await page.getByTestId("mx_EmojiPicker").locator(".mx_EmojiPicker_item", { hasText: "😇" }).click();
|
||||
|
||||
await page.locator(".mx_ContextualMenu_background").click(); // Close emoji picker
|
||||
await page.getByRole("textbox", { name: "Send a message…" }).press("Enter"); // Send message
|
||||
await page.getByRole("textbox", { name: "Send an unencrypted message…" }).press("Enter"); // Send message
|
||||
|
||||
await expect(page.locator(".mx_EventTile_body", { hasText: "😇" })).toBeVisible();
|
||||
});
|
||||
@@ -79,7 +79,7 @@ test.describe("Composer", () => {
|
||||
});
|
||||
|
||||
test("only sends when you press Control+Enter", async ({ page }) => {
|
||||
const composer = page.getByRole("textbox", { name: "Send a message…" });
|
||||
const composer = page.getByRole("textbox", { name: "Send an unencrypted message…" });
|
||||
// Type a message and press Enter
|
||||
await composer.pressSequentially("my message 3");
|
||||
await composer.press("Enter");
|
||||
|
||||
@@ -91,10 +91,10 @@ test.describe("Key backup reset from elsewhere", () => {
|
||||
|
||||
await csAPI.deleteBackupVersion(backupInfo.version);
|
||||
|
||||
await page.getByRole("textbox", { name: "Send an encrypted message…" }).fill("/discardsession");
|
||||
await page.getByRole("textbox", { name: "Send a message…" }).fill("/discardsession");
|
||||
await page.getByRole("button", { name: "Send message" }).click();
|
||||
|
||||
await page.getByRole("textbox", { name: "Send an encrypted message…" }).fill("Message with broken key backup");
|
||||
await page.getByRole("textbox", { name: "Send a message…" }).fill("Message with broken key backup");
|
||||
await page.getByRole("button", { name: "Send message" }).click();
|
||||
|
||||
// Should be the message we sent plus the room creation event
|
||||
|
||||
@@ -154,10 +154,12 @@ test.describe("Cryptography", function () {
|
||||
await app.client.bootstrapCrossSigning(aliceCredentials);
|
||||
await startDMWithBob(page, bob);
|
||||
// send first message
|
||||
await page.getByRole("textbox", { name: "Send a message…" }).fill("Hey!");
|
||||
await page.getByRole("textbox", { name: "Send a message…" }).press("Enter");
|
||||
await page.getByRole("textbox", { name: "Send an unencrypted message…" }).fill("Hey!");
|
||||
await page.getByRole("textbox", { name: "Send an unencrypted message…" }).press("Enter");
|
||||
await checkDMRoom(page);
|
||||
const bobRoomId = await bobJoin(page, bob);
|
||||
await expect(page.locator(".mx_MessageComposer_e2eIcon")).toMatchScreenshot("composer-e2e-icon-normal.png");
|
||||
|
||||
await testMessages(page, bob, bobRoomId);
|
||||
await verify(app, bob);
|
||||
|
||||
|
||||
@@ -124,6 +124,10 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||
const toasts = new Toasts(page);
|
||||
await toasts.rejectToast("Notifications");
|
||||
await toasts.assertNoToasts();
|
||||
|
||||
// There may still be a `/sendToDevice/m.secret.request` in flight, which will later throw an error and cause
|
||||
// a *subsequent* test to fail. Tell playwright to ignore any errors resulting from in-flight routes.
|
||||
await page.unrouteAll({ behavior: "ignoreErrors" });
|
||||
});
|
||||
|
||||
test("Verify device with QR code during login", async ({ page, app, credentials, homeserver }) => {
|
||||
@@ -205,7 +209,7 @@ test.describe("Device verification", { tag: "@no-webkit" }, () => {
|
||||
const dialog = page.locator(".mx_Dialog");
|
||||
// We use `pressSequentially` here to make sure that the FocusLock isn't causing us any problems
|
||||
// (cf https://github.com/element-hq/element-web/issues/30089)
|
||||
await dialog.locator("textarea").pressSequentially(recoveryKey);
|
||||
await dialog.getByTitle("Recovery key").pressSequentially(recoveryKey);
|
||||
await dialog.getByRole("button", { name: "Continue", disabled: false }).click();
|
||||
|
||||
await page.getByRole("button", { name: "Done" }).click();
|
||||
|
||||
@@ -228,7 +228,7 @@ export async function logIntoElement(page: Page, credentials: Credentials, secur
|
||||
await useSecurityKey.click();
|
||||
}
|
||||
// Fill in the recovery key
|
||||
await page.locator(".mx_Dialog").locator("textarea").fill(securityKey);
|
||||
await page.locator(".mx_Dialog").getByTitle("Recovery key").fill(securityKey);
|
||||
await page.getByRole("button", { name: "Continue", disabled: false }).click();
|
||||
await page.getByRole("button", { name: "Done" }).click();
|
||||
}
|
||||
@@ -263,7 +263,7 @@ export async function verifySession(app: ElementAppPage, securityKey: string) {
|
||||
const settings = await app.settings.openUserSettings("Encryption");
|
||||
await settings.getByRole("button", { name: "Verify this device" }).click();
|
||||
await app.page.getByRole("button", { name: "Verify with Recovery Key" }).click();
|
||||
await app.page.locator(".mx_Dialog").locator("textarea").fill(securityKey);
|
||||
await app.page.locator(".mx_Dialog").getByTitle("Recovery key").fill(securityKey);
|
||||
await app.page.getByRole("button", { name: "Continue", disabled: false }).click();
|
||||
await app.page.getByRole("button", { name: "Done" }).click();
|
||||
await app.settings.closeDialog();
|
||||
|
||||
@@ -30,6 +30,10 @@ test.describe("Lazy Loading", () => {
|
||||
});
|
||||
|
||||
test.beforeEach(async ({ page, homeserver, user, bot, app }) => {
|
||||
// The charlies were running off the bottom of the screen.
|
||||
// We no longer overscan the member list so the result is they are not in the dom.
|
||||
// Increase the viewport size to ensure they are.
|
||||
await page.setViewportSize({ width: 1000, height: 1000 });
|
||||
for (let i = 1; i <= 10; i++) {
|
||||
const displayName = `Charly #${i}`;
|
||||
const bot = new Bot(page, homeserver, { displayName, startClient: false, autoAcceptInvites: false });
|
||||
|
||||
@@ -35,8 +35,8 @@ test.describe("Header section of the room list", () => {
|
||||
|
||||
await expect(page.getByRole("menu")).toMatchScreenshot("room-list-header-compose-menu.png");
|
||||
|
||||
// New message should open the direct messages dialog
|
||||
await page.getByRole("menuitem", { name: "New message" }).click();
|
||||
// Start chat should open the direct messages dialog
|
||||
await page.getByRole("menuitem", { name: "Start chat" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Direct Messages" })).toBeVisible();
|
||||
await app.closeDialog();
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import { type Locator, type Page } from "@playwright/test";
|
||||
import { test, expect } from "../../element-web-test";
|
||||
|
||||
async function sendMessage(page: Page, message: string): Promise<Locator> {
|
||||
await page.getByRole("textbox", { name: "Send a message…" }).fill(message);
|
||||
await page.getByRole("textbox", { name: "Send an unencrypted message…" }).fill(message);
|
||||
await page.getByRole("button", { name: "Send message" }).click();
|
||||
|
||||
const msgTile = page.locator(".mx_EventTile_last");
|
||||
@@ -22,7 +22,7 @@ async function sendMessage(page: Page, message: string): Promise<Locator> {
|
||||
}
|
||||
|
||||
async function sendMultilineMessages(page: Page, messages: string[]) {
|
||||
await page.getByRole("textbox", { name: "Send a message…" }).focus();
|
||||
await page.getByRole("textbox", { name: "Send an unencrypted message…" }).focus();
|
||||
for (let i = 0; i < messages.length; i++) {
|
||||
await page.keyboard.type(messages[i]);
|
||||
if (i < messages.length - 1) await page.keyboard.press("Shift+Enter");
|
||||
@@ -40,7 +40,7 @@ async function replyMessage(page: Page, message: Locator, replyMessage: string):
|
||||
await line.hover();
|
||||
await line.getByRole("button", { name: "Reply", exact: true }).click();
|
||||
|
||||
await page.getByRole("textbox", { name: "Send a reply…" }).fill(replyMessage);
|
||||
await page.getByRole("textbox", { name: "Send an unencrypted reply…" }).fill(replyMessage);
|
||||
await page.getByRole("button", { name: "Send message" }).click();
|
||||
|
||||
const msgTile = page.locator(".mx_EventTile_last");
|
||||
|
||||
@@ -29,7 +29,7 @@ test.describe("Pills", () => {
|
||||
|
||||
// send a message using the built-in room mention functionality (autocomplete)
|
||||
await page
|
||||
.getByRole("textbox", { name: "Send a message…" })
|
||||
.getByRole("textbox", { name: "Send an unencrypted message…" })
|
||||
.pressSequentially(`Hello world! Join here: #${targetLocalpart.substring(0, 3)}`);
|
||||
await page.locator(".mx_Autocomplete_Completion_title").click();
|
||||
await page.getByRole("button", { name: "Send message" }).click();
|
||||
|
||||
@@ -63,9 +63,7 @@ test.describe("FilePanel", () => {
|
||||
await expect(roomViewBody.locator(".mx_EventTile[data-layout='group'] img[alt='riot.png']")).toBeVisible();
|
||||
|
||||
// Assert that the audio player is rendered
|
||||
await expect(
|
||||
roomViewBody.locator(".mx_EventTile[data-layout='group'] .mx_AudioPlayer_container"),
|
||||
).toBeVisible();
|
||||
await expect(roomViewBody.getByRole("region", { name: "Audio player" })).toBeVisible();
|
||||
|
||||
// Assert that the file button exists
|
||||
await expect(
|
||||
@@ -97,9 +95,7 @@ test.describe("FilePanel", () => {
|
||||
await expect(image.locator("img[alt='riot.png']")).toBeVisible();
|
||||
|
||||
// Detect the audio file
|
||||
const audio = filePanelMessageList.locator(
|
||||
".mx_EventTile_mediaLine .mx_MAudioBody .mx_AudioPlayer_container",
|
||||
);
|
||||
const audio = filePanelMessageList.getByRole("region", { name: "Audio player" });
|
||||
// Assert that the play button is rendered
|
||||
await expect(audio.getByRole("button", { name: "Play" })).toBeVisible();
|
||||
|
||||
@@ -130,7 +126,7 @@ test.describe("FilePanel", () => {
|
||||
// Take a snapshot of file tiles list on FilePanel
|
||||
await expect(filePanelMessageList).toMatchScreenshot("file-tiles-list.png", {
|
||||
// Exclude timestamps & flaky seek bar from snapshot
|
||||
mask: [page.locator(".mx_MessageTimestamp, .mx_AudioPlayer_seek")],
|
||||
mask: [page.locator(".mx_MessageTimestamp"), page.getByTestId("audio-player-seek")],
|
||||
});
|
||||
});
|
||||
|
||||
@@ -138,21 +134,19 @@ test.describe("FilePanel", () => {
|
||||
// Upload an image file
|
||||
await uploadFile(page, "playwright/sample-files/1sec.ogg");
|
||||
|
||||
const audioBody = page.locator(
|
||||
".mx_FilePanel .mx_RoomView_MessageList .mx_EventTile_mediaLine .mx_MAudioBody .mx_AudioPlayer_container",
|
||||
);
|
||||
const audioBody = page.getByTestId("right-panel").getByRole("region", { name: "Audio player" });
|
||||
|
||||
// Assert that the audio player is rendered
|
||||
// Assert that the audio file information is rendered
|
||||
const mediaInfo = audioBody.locator(".mx_AudioPlayer_mediaInfo");
|
||||
await expect(mediaInfo.locator(".mx_AudioPlayer_mediaName").getByText("1sec.ogg")).toBeVisible();
|
||||
await expect(mediaInfo.locator(".mx_AudioPlayer_byline", { hasText: "00:01" })).toBeVisible();
|
||||
await expect(mediaInfo.locator(".mx_AudioPlayer_byline", { hasText: "(3.56 KB)" })).toBeVisible(); // actual size
|
||||
// Assert that the audio file information is rendered;
|
||||
await expect(audioBody.getByText("1sec.ogg")).toBeVisible(); // extension
|
||||
await expect(audioBody.getByRole("time")).toHaveText("00:01"); // duration
|
||||
await expect(audioBody.getByText("(3.56 KB)")).toBeVisible(); // actual size;
|
||||
|
||||
// Assert that the duration counter is 00:01 before clicking the play button
|
||||
await expect(audioBody.locator(".mx_AudioPlayer_mediaInfo time", { hasText: "00:01" })).toBeVisible();
|
||||
await expect(audioBody.getByRole("time")).toHaveText("00:01");
|
||||
|
||||
// Assert that the counter is zero before clicking the play button
|
||||
await expect(audioBody.locator(".mx_AudioPlayer_seek [role='timer']", { hasText: "00:00" })).toBeVisible();
|
||||
await expect(audioBody.getByRole("timer")).toHaveText("00:00");
|
||||
|
||||
// Click the play button
|
||||
await audioBody.getByRole("button", { name: "Play" }).click();
|
||||
@@ -161,7 +155,7 @@ test.describe("FilePanel", () => {
|
||||
await expect(audioBody.getByRole("button", { name: "Pause" })).toBeVisible();
|
||||
|
||||
// Assert that the timer is reset when the audio file finished playing
|
||||
await expect(audioBody.locator(".mx_AudioPlayer_seek [role='timer']", { hasText: "00:00" })).toBeVisible();
|
||||
await expect(audioBody.getByRole("timer")).toHaveText("00:00");
|
||||
|
||||
// Assert that the play button is rendered
|
||||
await expect(audioBody.getByRole("button", { name: "Play" })).toBeVisible();
|
||||
|
||||
@@ -11,6 +11,32 @@ import { Bot } from "../../pages/bot";
|
||||
const ROOM_NAME = "Test room";
|
||||
const NAME = "Alice";
|
||||
|
||||
async function setupRoomWithMembers(
|
||||
app: any,
|
||||
page: any,
|
||||
homeserver: any,
|
||||
roomName: string,
|
||||
memberNames: string[],
|
||||
): Promise<string> {
|
||||
const visibility = await page.evaluate(() => (window as any).matrixcs.Visibility.Public);
|
||||
const id = await app.client.createRoom({ name: roomName, visibility });
|
||||
const bots: Bot[] = [];
|
||||
|
||||
for (let i = 0; i < memberNames.length; i++) {
|
||||
const displayName = memberNames[i];
|
||||
const bot = new Bot(page, homeserver, { displayName, startClient: false, autoAcceptInvites: false });
|
||||
if (displayName === "Susan") {
|
||||
await bot.prepareClient();
|
||||
await app.client.inviteUser(id, bot.credentials?.userId);
|
||||
} else {
|
||||
await bot.joinRoom(id);
|
||||
}
|
||||
bots.push(bot);
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
test.use({
|
||||
synapseConfig: {
|
||||
presence: {
|
||||
@@ -25,17 +51,8 @@ test.use({
|
||||
test.describe("Memberlist", () => {
|
||||
test.beforeEach(async ({ app, user, page, homeserver }, testInfo) => {
|
||||
testInfo.setTimeout(testInfo.timeout + 30_000);
|
||||
const id = await app.client.createRoom({ name: ROOM_NAME });
|
||||
const newBots: Bot[] = [];
|
||||
const names = ["Bob", "Bob", "Susan"];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const displayName = names[i];
|
||||
const autoAcceptInvites = displayName !== "Susan";
|
||||
const bot = new Bot(page, homeserver, { displayName, startClient: true, autoAcceptInvites });
|
||||
await bot.prepareClient();
|
||||
await app.client.inviteUser(id, bot.credentials?.userId);
|
||||
newBots.push(bot);
|
||||
}
|
||||
await setupRoomWithMembers(app, page, homeserver, ROOM_NAME, names);
|
||||
});
|
||||
|
||||
test("Renders correctly", { tag: "@screenshot" }, async ({ page, app }) => {
|
||||
@@ -45,4 +62,37 @@ test.describe("Memberlist", () => {
|
||||
await expect(memberlist.getByText("Invited")).toHaveCount(1);
|
||||
await expect(page.locator(".mx_MemberListView")).toMatchScreenshot("with-four-members.png");
|
||||
});
|
||||
|
||||
test("should handle scroll and click to view member profile", async ({ page, app, homeserver }) => {
|
||||
// Create a room with many members to enable scrolling
|
||||
const memberNames = Array.from({ length: 15 }, (_, i) => `Member${i.toString()}`);
|
||||
await setupRoomWithMembers(app, page, homeserver, "Large Room", memberNames);
|
||||
|
||||
// Navigate to the room and open member list
|
||||
await app.viewRoomByName("Large Room");
|
||||
|
||||
const memberlist = await app.toggleMemberlistPanel();
|
||||
|
||||
// Get the scrollable container
|
||||
const memberListContainer = memberlist.locator(".mx_AutoHideScrollbar");
|
||||
|
||||
// Scroll down to the bottom of the member list
|
||||
await app.scrollListToBottom(memberListContainer);
|
||||
|
||||
// Wait for the target member to be visible after scrolling
|
||||
const targetName = "Member14";
|
||||
const targetMember = memberlist.locator(".mx_MemberTileView_name").filter({ hasText: targetName });
|
||||
await targetMember.waitFor({ state: "visible" });
|
||||
|
||||
// Verify Alice is not visible at this point
|
||||
await expect(memberlist.locator(".mx_MemberTileView_name").filter({ hasText: "Alice" })).toHaveCount(0);
|
||||
|
||||
// Click on a member near the bottom of the list
|
||||
await expect(targetMember).toBeVisible();
|
||||
await targetMember.click();
|
||||
|
||||
// Verify that the user info screen is shown and hasn't scrolled back to top
|
||||
await expect(page.locator(".mx_UserInfo")).toBeVisible();
|
||||
await expect(page.locator(".mx_UserInfo_profile").getByText(targetName)).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,7 +35,7 @@ test.describe("Share dialog", () => {
|
||||
|
||||
const rightPanel = await app.toggleRoomInfoPanel();
|
||||
await rightPanel.getByRole("menuitem", { name: "People" }).click();
|
||||
await rightPanel.getByRole("button", { name: `${user.userId} (power 100)` }).click();
|
||||
await rightPanel.getByRole("option", { name: user.displayName }).click();
|
||||
await rightPanel.getByRole("button", { name: "Share profile" }).click();
|
||||
|
||||
const dialog = page.getByRole("dialog", { name: "Share User" });
|
||||
|
||||
@@ -23,7 +23,7 @@ async function openSpaceContextMenu(page: Page, app: ElementAppPage, spaceName:
|
||||
return page.locator(".mx_SpacePanel_contextMenu");
|
||||
}
|
||||
|
||||
function spaceCreateOptions(spaceName: string, roomIds: string[] = []): ICreateRoomOpts {
|
||||
function spaceCreateOptions(serverName: string, spaceName: string, roomIds: string[] = []): ICreateRoomOpts {
|
||||
return {
|
||||
creation_content: {
|
||||
type: "m.space",
|
||||
@@ -35,17 +35,21 @@ function spaceCreateOptions(spaceName: string, roomIds: string[] = []): ICreateR
|
||||
name: spaceName,
|
||||
},
|
||||
},
|
||||
...roomIds.map((r) => spaceChildInitialState(r)),
|
||||
...roomIds.map((r) => spaceChildInitialState(serverName, r)),
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function spaceChildInitialState(roomId: string, order?: string): ICreateRoomOpts["initial_state"]["0"] {
|
||||
function spaceChildInitialState(
|
||||
serverName: string,
|
||||
roomId: string,
|
||||
order?: string,
|
||||
): ICreateRoomOpts["initial_state"]["0"] {
|
||||
return {
|
||||
type: "m.space.child",
|
||||
state_key: roomId,
|
||||
content: {
|
||||
via: [roomId.split(":")[1]],
|
||||
via: [serverName],
|
||||
order,
|
||||
},
|
||||
};
|
||||
@@ -240,7 +244,7 @@ test.describe("Spaces", () => {
|
||||
});
|
||||
await expect(await app.getSpacePanelButton("My Space")).toBeVisible();
|
||||
|
||||
const roomId = await bot.createRoom(spaceCreateOptions("Space Space"));
|
||||
const roomId = await bot.createRoom(spaceCreateOptions(user.homeServer, "Space Space"));
|
||||
await bot.inviteUser(roomId, user.userId);
|
||||
|
||||
// Assert that `Space Space` is above `My Space` due to it being an invite
|
||||
@@ -260,7 +264,10 @@ test.describe("Spaces", () => {
|
||||
const spaceName = "Spacey Mc. Space Space";
|
||||
await app.client.createSpace({
|
||||
name: spaceName,
|
||||
initial_state: [spaceChildInitialState(roomId1), spaceChildInitialState(roomId2)],
|
||||
initial_state: [
|
||||
spaceChildInitialState(user.homeServer, roomId1),
|
||||
spaceChildInitialState(user.homeServer, roomId2),
|
||||
],
|
||||
});
|
||||
|
||||
await app.viewSpaceHomeByName(spaceName);
|
||||
@@ -287,7 +294,7 @@ test.describe("Spaces", () => {
|
||||
});
|
||||
await app.client.createSpace({
|
||||
name: "Root Space",
|
||||
initial_state: [spaceChildInitialState(childSpaceId)],
|
||||
initial_state: [spaceChildInitialState(user.homeServer, childSpaceId)],
|
||||
});
|
||||
|
||||
// Find collapsed Space panel
|
||||
@@ -323,7 +330,7 @@ test.describe("Spaces", () => {
|
||||
name: "Test Room",
|
||||
topic: "This is a topic https://github.com/matrix-org/matrix-react-sdk/pull/10060 with a link",
|
||||
});
|
||||
const spaceId = await bot.createRoom(spaceCreateOptions("Test Space", [roomId]));
|
||||
const spaceId = await bot.createRoom(spaceCreateOptions(user.homeServer, "Test Space", [roomId]));
|
||||
await bot.inviteUser(spaceId, user.userId);
|
||||
|
||||
await expect(await app.getSpacePanelButton("Test Space")).toBeVisible();
|
||||
@@ -361,9 +368,9 @@ test.describe("Spaces", () => {
|
||||
await app.client.createSpace({
|
||||
name: "Root Space",
|
||||
initial_state: [
|
||||
spaceChildInitialState(childSpaceId1, "a"),
|
||||
spaceChildInitialState(childSpaceId2, "b"),
|
||||
spaceChildInitialState(childSpaceId3, "c"),
|
||||
spaceChildInitialState(user.homeServer, childSpaceId1, "a"),
|
||||
spaceChildInitialState(user.homeServer, childSpaceId2, "b"),
|
||||
spaceChildInitialState(user.homeServer, childSpaceId3, "c"),
|
||||
],
|
||||
});
|
||||
await app.viewSpaceByName("Root Space");
|
||||
|
||||
@@ -30,7 +30,7 @@ async function startDM(app: ElementAppPage, page: Page, name: string): Promise<v
|
||||
await result.first().click();
|
||||
|
||||
// send first message to start DM
|
||||
const locator = page.getByRole("textbox", { name: "Send a message…" });
|
||||
const locator = page.getByRole("textbox", { name: "Send an unencrypted message…" });
|
||||
await expect(locator).toBeFocused();
|
||||
await locator.fill("Hey!");
|
||||
await locator.press("Enter");
|
||||
@@ -260,7 +260,7 @@ test.describe("Spotlight", () => {
|
||||
|
||||
// Send first message to actually start DM
|
||||
await expect(roomHeaderName(page)).toHaveText(bot2.credentials.displayName);
|
||||
const locator = page.getByRole("textbox", { name: "Send a message…" });
|
||||
const locator = page.getByRole("textbox", { name: "Send an unencrypted message…" });
|
||||
await locator.fill("Hey!");
|
||||
await locator.press("Enter");
|
||||
|
||||
|
||||
@@ -43,7 +43,7 @@ test.describe("Threads", () => {
|
||||
|
||||
const roomViewLocator = page.locator(".mx_RoomView_body");
|
||||
// User sends message
|
||||
const textbox = roomViewLocator.getByRole("textbox", { name: "Send a message…" });
|
||||
const textbox = roomViewLocator.getByRole("textbox", { name: "Send an unencrypted message…" });
|
||||
await textbox.fill("Hello Mr. Bot");
|
||||
await textbox.press("Enter");
|
||||
|
||||
@@ -108,7 +108,7 @@ test.describe("Threads", () => {
|
||||
await app.settings.setValue("layout", null, SettingLevel.DEVICE, Layout.Group);
|
||||
|
||||
// User responds in thread
|
||||
locator = page.locator(".mx_ThreadView").getByRole("textbox", { name: "Send a message…" });
|
||||
locator = page.locator(".mx_ThreadView").getByRole("textbox", { name: "Send an unencrypted message…" });
|
||||
await locator.fill("Test");
|
||||
await locator.press("Enter");
|
||||
|
||||
@@ -262,7 +262,7 @@ test.describe("Threads", () => {
|
||||
await locator.locator(".mx_EventTile_line").click();
|
||||
|
||||
// User responds & asserts
|
||||
locator = page.locator(".mx_ThreadView").getByRole("textbox", { name: "Send a message…" });
|
||||
locator = page.locator(".mx_ThreadView").getByRole("textbox", { name: "Send an unencrypted message…" });
|
||||
await locator.fill("Great!");
|
||||
await locator.press("Enter");
|
||||
|
||||
@@ -335,8 +335,8 @@ test.describe("Threads", () => {
|
||||
|
||||
// Send message
|
||||
const locator = page.locator(".mx_RoomView_body");
|
||||
await locator.getByRole("textbox", { name: "Send a message…" }).fill("Hello Mr. Bot");
|
||||
await locator.getByRole("textbox", { name: "Send a message…" }).press("Enter");
|
||||
await locator.getByRole("textbox", { name: "Send an unencrypted message…" }).fill("Hello Mr. Bot");
|
||||
await locator.getByRole("textbox", { name: "Send an unencrypted message…" }).press("Enter");
|
||||
// Create thread
|
||||
const locator2 = locator.locator(".mx_EventTile[data-scroll-tokens]").filter({ hasText: "Hello Mr. Bot" });
|
||||
await locator2.hover();
|
||||
@@ -366,7 +366,7 @@ test.describe("Threads", () => {
|
||||
|
||||
let locator = page.locator(".mx_RoomView_body");
|
||||
// User sends message
|
||||
let textbox = locator.getByRole("textbox", { name: "Send a message…" });
|
||||
let textbox = locator.getByRole("textbox", { name: "Send an unencrypted message…" });
|
||||
await textbox.fill("Hello Mr. Bot");
|
||||
await textbox.press("Enter");
|
||||
// Wait for message to send, get its ID and save as @threadId
|
||||
@@ -395,7 +395,7 @@ test.describe("Threads", () => {
|
||||
locator = page.locator(".mx_ThreadView");
|
||||
await locator.locator(".mx_EventTile_last").hover();
|
||||
await locator.locator(".mx_EventTile_last").getByRole("button", { name: "Reply" }).click();
|
||||
textbox = locator.getByRole("textbox", { name: "Reply to thread…" });
|
||||
textbox = locator.getByRole("textbox", { name: "Reply to unencrypted thread…" });
|
||||
await textbox.fill("Please come here");
|
||||
await textbox.press("Enter");
|
||||
// Wait until the reply is sent
|
||||
@@ -414,7 +414,7 @@ test.describe("Threads", () => {
|
||||
|
||||
// Send message
|
||||
let locator = page.locator(".mx_RoomView_body");
|
||||
let textbox = locator.getByRole("textbox", { name: "Send a message…" });
|
||||
let textbox = locator.getByRole("textbox", { name: "Send an unencrypted message…" });
|
||||
await textbox.fill("Hello Mr. Bot");
|
||||
await textbox.press("Enter");
|
||||
// Create thread
|
||||
@@ -425,7 +425,7 @@ test.describe("Threads", () => {
|
||||
|
||||
// Send message to thread
|
||||
locator = page.locator(".mx_ThreadPanel");
|
||||
textbox = locator.getByRole("textbox", { name: "Send a message…" });
|
||||
textbox = locator.getByRole("textbox", { name: "Send an unencrypted message…" });
|
||||
await textbox.fill("Hello Mr. User");
|
||||
await textbox.press("Enter");
|
||||
await expect(locator.locator(".mx_EventTile_last").getByText("Hello Mr. User")).toBeAttached();
|
||||
@@ -456,7 +456,7 @@ test.describe("Threads", () => {
|
||||
*/
|
||||
const sendMessage = async (message: string) => {
|
||||
const messageComposer = page.getByRole("region", { name: "Message composer" });
|
||||
const textbox = messageComposer.getByRole("textbox", { name: "Send a message…" });
|
||||
const textbox = messageComposer.getByRole("textbox", { name: "Send an unencrypted message…" });
|
||||
await textbox.fill(message);
|
||||
await textbox.press("Enter");
|
||||
};
|
||||
@@ -478,7 +478,7 @@ test.describe("Threads", () => {
|
||||
|
||||
// Send a message in the thread
|
||||
const threadPanel = page.locator(".mx_ThreadPanel");
|
||||
const textbox = threadPanel.getByRole("textbox", { name: "Send a message…" });
|
||||
const textbox = threadPanel.getByRole("textbox", { name: "Send an unencrypted message…" });
|
||||
await textbox.fill(threadMessage);
|
||||
await textbox.press("Enter");
|
||||
await expect(threadPanel.locator(".mx_EventTile_last").getByText(threadMessage)).toBeVisible();
|
||||
|
||||
@@ -461,11 +461,11 @@ test.describe("Timeline", () => {
|
||||
// Send a emote
|
||||
await page
|
||||
.locator(".mx_RoomView_body")
|
||||
.getByRole("textbox", { name: "Send a message…" })
|
||||
.getByRole("textbox", { name: "Send an unencrypted message…" })
|
||||
.fill("/me says hello to Mr. Bot");
|
||||
await page
|
||||
.locator(".mx_RoomView_body")
|
||||
.getByRole("textbox", { name: "Send a message…" })
|
||||
.getByRole("textbox", { name: "Send an unencrypted message…" })
|
||||
.press("Enter");
|
||||
// Check inline start margin of its avatar
|
||||
// Here --right-padding is for the avatar on the message line
|
||||
|
||||
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 13 KiB |
|
After Width: | Height: | Size: 9.9 KiB |
|
After Width: | Height: | Size: 9.1 KiB |
|
After Width: | Height: | Size: 5.3 KiB |
|
After Width: | Height: | Size: 6.6 KiB |
|
After Width: | Height: | Size: 5.1 KiB |
|
After Width: | Height: | Size: 4.9 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 4.4 KiB |
|
After Width: | Height: | Size: 7.7 KiB |
|
Before Width: | Height: | Size: 6.0 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 8.6 KiB After Width: | Height: | Size: 8.4 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 6.1 KiB After Width: | Height: | Size: 5.7 KiB |
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 8.2 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.5 KiB |
|
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 5.5 KiB |
|
Before Width: | Height: | Size: 8.7 KiB After Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 7.9 KiB After Width: | Height: | Size: 7.4 KiB |
|
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 4.9 KiB |
|
Before Width: | Height: | Size: 7.8 KiB After Width: | Height: | Size: 7.3 KiB |
|
Before Width: | Height: | Size: 7.0 KiB After Width: | Height: | Size: 6.5 KiB |
|
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 9.3 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 16 KiB |
|
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 17 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 12 KiB After Width: | Height: | Size: 12 KiB |
|
After Width: | Height: | Size: 268 B |
|
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 12 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 23 KiB |
|
Before Width: | Height: | Size: 4.5 KiB After Width: | Height: | Size: 4.3 KiB |
|
Before Width: | Height: | Size: 90 KiB After Width: | Height: | Size: 89 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 24 KiB |
|
Before Width: | Height: | Size: 103 KiB After Width: | Height: | Size: 104 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 18 KiB |
|
Before Width: | Height: | Size: 27 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 28 KiB After Width: | Height: | Size: 28 KiB |
|
Before Width: | Height: | Size: 18 KiB After Width: | Height: | Size: 19 KiB |
|
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 33 KiB |
|
Before Width: | Height: | Size: 29 KiB After Width: | Height: | Size: 30 KiB |
|
Before Width: | Height: | Size: 30 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 54 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 48 KiB |
|
Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 8.0 KiB After Width: | Height: | Size: 8.0 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 56 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 64 KiB |
|
Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 69 KiB After Width: | Height: | Size: 70 KiB |
|
Before Width: | Height: | Size: 62 KiB After Width: | Height: | Size: 62 KiB |
|
Before Width: | Height: | Size: 59 KiB After Width: | Height: | Size: 61 KiB |
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 56 KiB After Width: | Height: | Size: 57 KiB |
|
Before Width: | Height: | Size: 6.4 KiB After Width: | Height: | Size: 6.3 KiB |
@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
import { SynapseContainer as BaseSynapseContainer } from "@element-hq/element-web-playwright-common/lib/testcontainers";
|
||||
|
||||
const TAG = "develop@sha256:8c2d9a93dd209a79d3e5e50cd18addfe52d80bea0ffe48a5d3e15836032eeb9d";
|
||||
const TAG = "develop@sha256:a0d8db97e39321166959cecdc4c684daff36fefdc7968de47be7e0fdb1cb4541";
|
||||
|
||||
/**
|
||||
* SynapseContainer which freezes the docker digest to stabilise tests,
|
||||
|
||||
@@ -95,7 +95,6 @@
|
||||
@import "./structures/auth/_Registration.pcss";
|
||||
@import "./structures/auth/_SessionLockStolenView.pcss";
|
||||
@import "./structures/auth/_SetupEncryptionBody.pcss";
|
||||
@import "./views/audio_messages/_AudioPlayer.pcss";
|
||||
@import "./views/audio_messages/_PlayPauseButton.pcss";
|
||||
@import "./views/audio_messages/_PlaybackContainer.pcss";
|
||||
@import "./views/audio_messages/_SeekBar.pcss";
|
||||
|
||||
@@ -15,7 +15,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
display: grid;
|
||||
justify-content: left;
|
||||
align-items: center;
|
||||
grid-gap: $spacing-8;
|
||||
gap: $spacing-8;
|
||||
grid-template-columns: auto auto auto;
|
||||
grid-template-rows: auto;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -22,7 +22,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
display: grid;
|
||||
justify-content: left;
|
||||
align-items: center;
|
||||
grid-gap: $spacing-8;
|
||||
gap: $spacing-8;
|
||||
grid-template-columns: min-content 1fr min-content;
|
||||
grid-template-rows: auto;
|
||||
}
|
||||
@@ -47,7 +47,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
.mx_PollListItemEnded_answers {
|
||||
display: grid;
|
||||
grid-gap: $spacing-8;
|
||||
gap: $spacing-8;
|
||||
margin-top: $spacing-12;
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
.mx_DeviceDetailHeading_renameForm {
|
||||
display: grid;
|
||||
grid-gap: $spacing-16;
|
||||
gap: $spacing-16;
|
||||
justify-content: left;
|
||||
grid-template-columns: 100%;
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
border-bottom: 1px solid $quinary-content;
|
||||
|
||||
display: grid;
|
||||
grid-gap: $spacing-24;
|
||||
gap: $spacing-24;
|
||||
justify-content: left;
|
||||
grid-template-columns: 100%;
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
.mx_DeviceTile_actions {
|
||||
display: grid;
|
||||
grid-gap: $spacing-8;
|
||||
gap: $spacing-8;
|
||||
grid-auto-flow: column;
|
||||
margin-left: $spacing-8;
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
.mx_FilteredDeviceList_list {
|
||||
list-style-type: none;
|
||||
display: grid;
|
||||
grid-gap: $spacing-16;
|
||||
gap: $spacing-16;
|
||||
margin: 0;
|
||||
padding: 0 $spacing-16;
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
.mx_SettingsSubsection_content {
|
||||
width: 100%;
|
||||
display: grid;
|
||||
grid-gap: $spacing-8;
|
||||
gap: $spacing-8;
|
||||
/* setting minwidth 0 makes columns definitely sized fixing horizontal overflow */
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
justify-items: flex-start;
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
.mx_MediaBody.mx_AudioPlayer_container {
|
||||
padding: 16px 12px 12px 12px;
|
||||
|
||||
.mx_AudioPlayer_primaryContainer {
|
||||
display: flex;
|
||||
|
||||
.mx_PlayPauseButton {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.mx_AudioPlayer_mediaInfo {
|
||||
flex: 1;
|
||||
overflow: hidden; /* makes the ellipsis on the file name work */
|
||||
|
||||
& > * {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.mx_AudioPlayer_mediaName {
|
||||
color: $primary-content;
|
||||
font-size: $font-15px;
|
||||
line-height: $font-15px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
padding-bottom: 4px; /* mimics the line-height differences in the Figma */
|
||||
}
|
||||
|
||||
.mx_AudioPlayer_byline {
|
||||
font-size: $font-12px;
|
||||
line-height: $font-12px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_AudioPlayer_seek {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
.mx_SeekBar {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.mx_Clock {
|
||||
min-width: $font-42px; /* for flexbox */
|
||||
padding-left: $spacing-4; /* isolate from seek bar */
|
||||
text-align: justify;
|
||||
white-space: nowrap;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,23 @@ Please see LICENSE files in the repository root for full details.
|
||||
}
|
||||
|
||||
.mx_AccessSecretStorageDialog_primaryContainer {
|
||||
.mx_AccessSecretStorageDialog_recoveryKeyEntry {
|
||||
/*
|
||||
* Be specific here to avoid "margin: 9px" from _common.pcss
|
||||
*/
|
||||
:not(.mx_textinput):not(.mx_Field):not(.mx_no_textinput) {
|
||||
input {
|
||||
/*
|
||||
* From figma: https://www.figma.com/design/ZodBLtGnKmRTGJo5SGLnH3/ER-137--Excluding-Insecure-Devices?node-id=102-43729&t=QmewENUd7f6Tmw9U-1
|
||||
*/
|
||||
width: 448px;
|
||||
height: 70px;
|
||||
margin: 0px;
|
||||
border: 1px solid;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.mx_AccessSecretStorageDialog_recoveryKeyFeedback {
|
||||
&::before {
|
||||
content: "";
|
||||
|
||||
@@ -60,7 +60,7 @@ Please see LICENSE files in the repository root for full details.
|
||||
|
||||
.mx_MPollBody_allOptions {
|
||||
display: grid;
|
||||
grid-gap: $spacing-16;
|
||||
gap: $spacing-16;
|
||||
margin-bottom: $spacing-8;
|
||||
max-width: 550px;
|
||||
}
|
||||
|
||||