From ae600da287600eb420f2cfbb3a5e044a1a12b72e Mon Sep 17 00:00:00 2001
From: Henry Jameson
Date: Tue, 6 Jan 2026 18:38:00 +0200
Subject: [PATCH 01/17] fix tests
---
test/fixtures/mock_api.js | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/test/fixtures/mock_api.js b/test/fixtures/mock_api.js
index b2cd16982..03fb01a64 100644
--- a/test/fixtures/mock_api.js
+++ b/test/fixtures/mock_api.js
@@ -8,7 +8,8 @@ export const injectMswToTest = (defaultHandlers) => {
return testBase.extend({
worker: [
- async (_, use) => {
+ // biome-ignore lint: required by vitest
+ async ({}, use) => {
await worker.start()
await use(worker)
From 7ffec2c3246d3bb76b89c27b64cdee60684ff1ee Mon Sep 17 00:00:00 2001
From: Lain Soykaf
Date: Wed, 7 Jan 2026 09:44:50 +0400
Subject: [PATCH 02/17] Add docker-compose Playwright E2E stack
---
.dockerignore | 12 ++
.gitignore | 2 +
.gitlab-ci.yml | 28 +++++
docker-compose.e2e.yml | 57 +++++++++
docker/e2e/Dockerfile.e2e | 17 +++
docker/pleroma/entrypoint.e2e.sh | 57 +++++++++
package.json | 7 +-
.../settings_modal/helpers/rate_setting.vue | 109 +++++++++---------
test/e2e-playwright/playwright.config.mjs | 49 ++++++++
test/e2e-playwright/specs/admin_smoke.spec.js | 25 ++++
tools/e2e/run.sh | 32 +++++
vite.config.js | 29 +++--
12 files changed, 359 insertions(+), 65 deletions(-)
create mode 100644 .dockerignore
create mode 100644 docker-compose.e2e.yml
create mode 100644 docker/e2e/Dockerfile.e2e
create mode 100644 docker/pleroma/entrypoint.e2e.sh
create mode 100644 test/e2e-playwright/playwright.config.mjs
create mode 100644 test/e2e-playwright/specs/admin_smoke.spec.js
create mode 100644 tools/e2e/run.sh
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 000000000..3de57a360
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,12 @@
+node_modules/
+dist/
+logs/
+.DS_Store
+.git/
+config/local.json
+pleroma-backend/
+test/e2e/reports/
+test/e2e-playwright/test-results/
+test/e2e-playwright/playwright-report/
+__screenshots__/
+
diff --git a/.gitignore b/.gitignore
index 01ffda9a8..085805753 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,8 @@ dist/
npm-debug.log
test/unit/coverage
test/e2e/reports
+test/e2e-playwright/test-results
+test/e2e-playwright/playwright-report
selenium-debug.log
.idea/
config/local.json
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 99c85dd36..3067af58b 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -60,6 +60,34 @@ test:
- test/**/__screenshots__
when: on_failure
+e2e-pleroma:
+ stage: test
+ image: docker:26
+ services:
+ - name: docker:26-dind
+ tags:
+ - amd64
+ - himem
+ variables:
+ DOCKER_HOST: tcp://docker:2375
+ DOCKER_TLS_CERTDIR: ""
+ DOCKER_DRIVER: overlay2
+ script:
+ - docker version
+ - docker compose version
+ - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
+ - export PLEROMA_IMAGE="${PLEROMA_IMAGE:-$CI_REGISTRY/pleroma/pleroma:stable}"
+ - docker compose -f docker-compose.e2e.yml up --build --remove-orphans --abort-on-container-exit --exit-code-from e2e
+ after_script:
+ - docker compose -f docker-compose.e2e.yml cp e2e:/app/test/e2e-playwright/test-results test/e2e-playwright/test-results || true
+ - docker compose -f docker-compose.e2e.yml cp e2e:/app/test/e2e-playwright/playwright-report test/e2e-playwright/playwright-report || true
+ - docker compose -f docker-compose.e2e.yml down -v --remove-orphans || true
+ artifacts:
+ when: on_failure
+ paths:
+ - test/e2e-playwright/test-results
+ - test/e2e-playwright/playwright-report
+
build:
stage: build
tags:
diff --git a/docker-compose.e2e.yml b/docker-compose.e2e.yml
new file mode 100644
index 000000000..75a4979a1
--- /dev/null
+++ b/docker-compose.e2e.yml
@@ -0,0 +1,57 @@
+services:
+ db:
+ image: postgres:15-alpine
+ environment:
+ POSTGRES_USER: pleroma
+ POSTGRES_PASSWORD: pleroma
+ POSTGRES_DB: pleroma
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U pleroma -d pleroma"]
+ interval: 2s
+ timeout: 2s
+ retries: 30
+
+ pleroma:
+ image: ${PLEROMA_IMAGE:-git.pleroma.social:5050/pleroma/pleroma:stable}
+ environment:
+ DB_USER: pleroma
+ DB_PASS: pleroma
+ DB_NAME: pleroma
+ DB_HOST: db
+ DB_PORT: 5432
+ DOMAIN: localhost
+ INSTANCE_NAME: Pleroma E2E
+ ADMIN_EMAIL: ${E2E_ADMIN_EMAIL:-admin@example.com}
+ NOTIFY_EMAIL: ${E2E_ADMIN_EMAIL:-admin@example.com}
+ E2E_ADMIN_USERNAME: ${E2E_ADMIN_USERNAME:-admin}
+ E2E_ADMIN_PASSWORD: ${E2E_ADMIN_PASSWORD:-adminadmin}
+ E2E_ADMIN_EMAIL: ${E2E_ADMIN_EMAIL:-admin@example.com}
+ depends_on:
+ db:
+ condition: service_healthy
+ volumes:
+ - ./docker/pleroma/entrypoint.e2e.sh:/opt/pleroma/entrypoint.e2e.sh:ro
+ entrypoint: ["/bin/ash", "/opt/pleroma/entrypoint.e2e.sh"]
+ healthcheck:
+ # NOTE: "localhost" may resolve to ::1 in some images (IPv6) while Pleroma only
+ # listens on IPv4 in this container. Use 127.0.0.1 to avoid false negatives.
+ test: ["CMD-SHELL", "test -f /var/lib/pleroma/.e2e_seeded && wget -qO- http://127.0.0.1:4000/api/v1/instance >/dev/null || exit 1"]
+ interval: 5s
+ timeout: 3s
+ retries: 60
+
+ e2e:
+ build:
+ context: .
+ dockerfile: docker/e2e/Dockerfile.e2e
+ depends_on:
+ pleroma:
+ condition: service_healthy
+ environment:
+ CI: "1"
+ VITE_PROXY_TARGET: http://pleroma:4000
+ VITE_PROXY_ORIGIN: http://localhost:4000
+ E2E_BASE_URL: http://localhost:8080
+ E2E_ADMIN_USERNAME: ${E2E_ADMIN_USERNAME:-admin}
+ E2E_ADMIN_PASSWORD: ${E2E_ADMIN_PASSWORD:-adminadmin}
+ command: ["yarn", "e2e:pw"]
diff --git a/docker/e2e/Dockerfile.e2e b/docker/e2e/Dockerfile.e2e
new file mode 100644
index 000000000..ec780e894
--- /dev/null
+++ b/docker/e2e/Dockerfile.e2e
@@ -0,0 +1,17 @@
+FROM mcr.microsoft.com/playwright:v1.55.0-jammy
+
+WORKDIR /app
+
+ENV PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
+
+RUN npm install -g yarn@1.22.22
+
+COPY package.json yarn.lock ./
+RUN yarn --frozen-lockfile
+
+COPY . .
+
+ENV CI=1
+
+CMD ["yarn", "e2e:pw"]
+
diff --git a/docker/pleroma/entrypoint.e2e.sh b/docker/pleroma/entrypoint.e2e.sh
new file mode 100644
index 000000000..6945cf5b6
--- /dev/null
+++ b/docker/pleroma/entrypoint.e2e.sh
@@ -0,0 +1,57 @@
+#!/bin/ash
+
+set -eu
+
+SEED_SENTINEL_PATH="/var/lib/pleroma/.e2e_seeded"
+
+echo "-- Waiting for database..."
+while ! pg_isready -U "${DB_USER:-pleroma}" -d "postgres://${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-pleroma}" -t 1; do
+ sleep 1s
+done
+
+echo "-- Running migrations..."
+/opt/pleroma/bin/pleroma_ctl migrate
+
+echo "-- Starting!"
+/opt/pleroma/bin/pleroma start &
+PLEROMA_PID="$!"
+
+cleanup() {
+ if [ -n "${PLEROMA_PID:-}" ] && kill -0 "$PLEROMA_PID" 2>/dev/null; then
+ kill -TERM "$PLEROMA_PID"
+ wait "$PLEROMA_PID" || true
+ fi
+}
+
+trap cleanup INT TERM
+
+echo "-- Waiting for API..."
+api_ok="false"
+for _i in $(seq 1 120); do
+ if wget -qO- http://127.0.0.1:4000/api/v1/instance >/dev/null 2>&1; then
+ api_ok="true"
+ break
+ fi
+ sleep 1s
+done
+
+if [ "$api_ok" != "true" ]; then
+ echo "Timed out waiting for Pleroma API to become available"
+ exit 1
+fi
+
+if [ ! -f "$SEED_SENTINEL_PATH" ]; then
+ if [ -n "${E2E_ADMIN_USERNAME:-}" ] && [ -n "${E2E_ADMIN_PASSWORD:-}" ] && [ -n "${E2E_ADMIN_EMAIL:-}" ]; then
+ echo "-- Seeding admin user (${E2E_ADMIN_USERNAME})..."
+ if ! /opt/pleroma/bin/pleroma_ctl user new "$E2E_ADMIN_USERNAME" "$E2E_ADMIN_EMAIL" --admin --password "$E2E_ADMIN_PASSWORD" -y; then
+ echo "-- User already exists (or creation failed), ensuring admin + confirmed..."
+ /opt/pleroma/bin/pleroma_ctl user set "$E2E_ADMIN_USERNAME" --admin --confirmed
+ fi
+ else
+ echo "-- Skipping admin seeding (missing E2E_ADMIN_* env)"
+ fi
+
+ touch "$SEED_SENTINEL_PATH"
+fi
+
+wait "$PLEROMA_PID"
diff --git a/package.json b/package.json
index 21d75f0fb..19cd5be6a 100644
--- a/package.json
+++ b/package.json
@@ -10,11 +10,12 @@
"unit": "node build/update-emoji.js && vitest --run",
"unit-ci": "node build/update-emoji.js && vitest --run --browser.headless",
"unit:watch": "node build/update-emoji.js && vitest",
- "e2e": "node test/e2e/runner.js",
+ "e2e:pw": "playwright test --config test/e2e-playwright/playwright.config.mjs",
+ "e2e": "sh ./tools/e2e/run.sh",
"test": "yarn run unit && yarn run e2e",
"stylelint": "yarn exec stylelint '**/*.scss' '**/*.vue'",
- "lint": "eslint src test/unit/specs test/e2e/specs",
- "lint-fix": "eslint --fix src test/unit/specs test/e2e/specs"
+ "lint": "eslint src test/unit/specs test/e2e/specs test/e2e-playwright/specs test/e2e-playwright/playwright.config.mjs",
+ "lint-fix": "eslint --fix src test/unit/specs test/e2e/specs test/e2e-playwright/specs test/e2e-playwright/playwright.config.mjs"
},
"dependencies": {
"@babel/runtime": "7.28.4",
diff --git a/src/components/settings_modal/helpers/rate_setting.vue b/src/components/settings_modal/helpers/rate_setting.vue
index f6216223c..49e09e046 100644
--- a/src/components/settings_modal/helpers/rate_setting.vue
+++ b/src/components/settings_modal/helpers/rate_setting.vue
@@ -30,60 +30,61 @@
-
- | |
-
- {{ $t('admin_dash.rate_limit.period') }}
- |
-
- {{ $t('admin_dash.rate_limit.amount') }}
- |
-
-
- |
- {{ $t('admin_dash.rate_limit.unauthenticated') }}
- |
-
- {{ $t('admin_dash.rate_limit.rate_limit') }}
- |
-
- update({ event: e, index: 0, side: 0, eventType: 'edit' })"
- >
- |
-
- update({ event: e, index: 1, side: 0, eventType: 'edit' })"
- >
- |
-
-
- |
- {{ $t('admin_dash.rate_limit.authenticated') }}
- |
-
- update({ event: e, index: 0, side: 1, eventType: 'edit' })"
- >
- |
-
- update({ event: e, index: 1, side: 1, eventType: 'edit' })"
- >
- |
-
+
+
+ | |
+
+ {{ $t('admin_dash.rate_limit.period') }}
+ |
+
+ {{ $t('admin_dash.rate_limit.amount') }}
+ |
+
+
+
+
+ |
+ {{ isSeparate ? $t('admin_dash.rate_limit.unauthenticated') : $t('admin_dash.rate_limit.rate_limit') }}
+ |
+
+ update({ event: e, index: 0, side: 0, eventType: 'edit' })"
+ >
+ |
+
+ update({ event: e, index: 1, side: 0, eventType: 'edit' })"
+ >
+ |
+
+
+ |
+ {{ $t('admin_dash.rate_limit.authenticated') }}
+ |
+
+ update({ event: e, index: 0, side: 1, eventType: 'edit' })"
+ >
+ |
+
+ update({ event: e, index: 1, side: 1, eventType: 'edit' })"
+ >
+ |
+
+
{
+ await page.goto('/login')
+
+ const loginForm = page.locator('#main-scroller form.login-form')
+ await loginForm.locator('#username').fill(adminUsername)
+ await loginForm.locator('#password').fill(adminPassword)
+ await loginForm.getByRole('button', { name: 'Log in' }).click()
+
+ await page.waitForURL(/\/main\/friends/)
+
+ await expect(page.getByTitle('Administration')).toBeVisible()
+ await page.getByTitle('Administration').click()
+
+ const modal = page.locator('.settings-modal-panel')
+ await expect(modal.getByRole('heading', { name: 'Administration' })).toBeVisible()
+
+ await modal.getByRole('tab', { name: 'Emoji' }).click()
+ await expect(modal.getByText('Emoji packs')).toBeVisible()
+})
diff --git a/tools/e2e/run.sh b/tools/e2e/run.sh
new file mode 100644
index 000000000..7cf4db10b
--- /dev/null
+++ b/tools/e2e/run.sh
@@ -0,0 +1,32 @@
+#!/bin/sh
+
+set -u
+
+COMPOSE_FILE="docker-compose.e2e.yml"
+
+: "${PLEROMA_IMAGE:=git.pleroma.social:5050/pleroma/pleroma:stable}"
+: "${E2E_ADMIN_USERNAME:=admin}"
+: "${E2E_ADMIN_PASSWORD:=adminadmin}"
+: "${E2E_ADMIN_EMAIL:=admin@example.com}"
+
+cleanup() {
+ docker compose -f "$COMPOSE_FILE" down -v --remove-orphans >/dev/null 2>&1 || true
+}
+
+cleanup
+
+trap 'cleanup; exit 130' INT TERM
+
+set +e
+docker compose -f "$COMPOSE_FILE" up --build --remove-orphans --abort-on-container-exit --exit-code-from e2e
+result="$?"
+set -e
+
+if [ "$result" -ne 0 ]; then
+ docker compose -f "$COMPOSE_FILE" cp e2e:/app/test/e2e-playwright/test-results test/e2e-playwright/test-results >/dev/null 2>&1 || true
+ docker compose -f "$COMPOSE_FILE" cp e2e:/app/test/e2e-playwright/playwright-report test/e2e-playwright/playwright-report >/dev/null 2>&1 || true
+fi
+
+cleanup
+
+exit "$result"
diff --git a/vite.config.js b/vite.config.js
index 0b4d0db90..ca48ce3bf 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -12,20 +12,32 @@ import { getCommitHash } from './build/commit_hash.js'
import mswPlugin from './build/msw_plugin.js'
const localConfigPath = '/config/local.json'
+const normalizeTarget = (target) => {
+ if (!target || typeof target !== 'string') return target
+ return target.endsWith('/') ? target.replace(/\/$/, '') : target
+}
+
const getLocalDevSettings = async () => {
+ const envTarget = normalizeTarget(process.env.VITE_PROXY_TARGET)
+ const envOrigin = normalizeTarget(process.env.VITE_PROXY_ORIGIN)
try {
const settings = (await import('./config/local.json')).default
- if (settings.target && settings.target.endsWith('/')) {
- // replacing trailing slash since it can conflict with some apis
- // and that's how actual BE reports its url
- settings.target = settings.target.replace(/\/$/, '')
- }
+ settings.target = normalizeTarget(settings.target)
+ settings.origin = normalizeTarget(settings.origin)
+ if (envTarget) settings.target = envTarget
+ if (envOrigin) settings.origin = envOrigin
console.info(`Using local dev server settings (${localConfigPath}):`)
console.info(JSON.stringify(settings, null, 2))
return settings
} catch (e) {
- console.info(`Local dev server settings not found (${localConfigPath}), using default`, e)
- return {}
+ if (!envTarget && !envOrigin) {
+ console.info(`Local dev server settings not found (${localConfigPath}), using default`, e)
+ return {}
+ }
+ const settings = { target: envTarget, origin: envOrigin }
+ console.info('Using dev server settings from VITE_PROXY_TARGET/VITE_PROXY_ORIGIN:')
+ console.info(JSON.stringify(settings, null, 2))
+ return settings
}
}
@@ -51,6 +63,7 @@ const getTransformSWSettings = (settings) => {
export default defineConfig(async ({ mode, command }) => {
const settings = await getLocalDevSettings()
const target = settings.target || 'http://localhost:4000/'
+ const origin = settings.origin || target
const transformSW = getTransformSWSettings(settings)
const proxy = {
'/api': {
@@ -70,7 +83,7 @@ export default defineConfig(async ({ mode, command }) => {
cookieDomainRewrite: 'localhost',
ws: true,
headers: {
- 'Origin': target
+ 'Origin': origin
}
},
'/oauth': {
From 99d2efac6cc789edfa1d5962fe300fbcab95b819 Mon Sep 17 00:00:00 2001
From: Lain Soykaf
Date: Wed, 7 Jan 2026 10:00:18 +0400
Subject: [PATCH 03/17] Reduce E2E output to Playwright logs
---
.gitlab-ci.yml | 3 ++-
tools/e2e/run.sh | 5 ++++-
2 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 3067af58b..399b9c644 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -72,12 +72,13 @@ e2e-pleroma:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
DOCKER_DRIVER: overlay2
+ COMPOSE_MENU: "false"
script:
- docker version
- docker compose version
- echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
- export PLEROMA_IMAGE="${PLEROMA_IMAGE:-$CI_REGISTRY/pleroma/pleroma:stable}"
- - docker compose -f docker-compose.e2e.yml up --build --remove-orphans --abort-on-container-exit --exit-code-from e2e
+ - docker compose -f docker-compose.e2e.yml up --build --remove-orphans --attach e2e --no-log-prefix --abort-on-container-exit --exit-code-from e2e
after_script:
- docker compose -f docker-compose.e2e.yml cp e2e:/app/test/e2e-playwright/test-results test/e2e-playwright/test-results || true
- docker compose -f docker-compose.e2e.yml cp e2e:/app/test/e2e-playwright/playwright-report test/e2e-playwright/playwright-report || true
diff --git a/tools/e2e/run.sh b/tools/e2e/run.sh
index 7cf4db10b..5ff2637ce 100644
--- a/tools/e2e/run.sh
+++ b/tools/e2e/run.sh
@@ -4,6 +4,7 @@ set -u
COMPOSE_FILE="docker-compose.e2e.yml"
+: "${COMPOSE_MENU:=false}"
: "${PLEROMA_IMAGE:=git.pleroma.social:5050/pleroma/pleroma:stable}"
: "${E2E_ADMIN_USERNAME:=admin}"
: "${E2E_ADMIN_PASSWORD:=adminadmin}"
@@ -18,11 +19,13 @@ cleanup
trap 'cleanup; exit 130' INT TERM
set +e
-docker compose -f "$COMPOSE_FILE" up --build --remove-orphans --abort-on-container-exit --exit-code-from e2e
+COMPOSE_MENU="$COMPOSE_MENU" docker compose -f "$COMPOSE_FILE" up --build --remove-orphans --attach e2e --no-log-prefix --abort-on-container-exit --exit-code-from e2e
result="$?"
set -e
if [ "$result" -ne 0 ]; then
+ mkdir -p test/e2e-playwright/test-results
+ docker compose -f "$COMPOSE_FILE" logs --no-color pleroma db > test/e2e-playwright/test-results/docker-compose.log 2>/dev/null || true
docker compose -f "$COMPOSE_FILE" cp e2e:/app/test/e2e-playwright/test-results test/e2e-playwright/test-results >/dev/null 2>&1 || true
docker compose -f "$COMPOSE_FILE" cp e2e:/app/test/e2e-playwright/playwright-report test/e2e-playwright/playwright-report >/dev/null 2>&1 || true
fi
From 7f547068214d5c84c59615f7959e0219043adecb Mon Sep 17 00:00:00 2001
From: Lain Soykaf
Date: Wed, 7 Jan 2026 10:50:26 +0400
Subject: [PATCH 04/17] E2E: add user smoke tests
- Disable captcha + open registrations in E2E Pleroma config
- Await login after signup to avoid redirect race
- Add basic register/login/post smoke specs
- Fix failure artifact copying order
---
docker/pleroma/entrypoint.e2e.sh | 14 ++++
src/modules/users.js | 2 +-
test/e2e-playwright/specs/user_smoke.spec.js | 80 ++++++++++++++++++++
tools/e2e/run.sh | 4 +-
4 files changed, 97 insertions(+), 3 deletions(-)
create mode 100644 test/e2e-playwright/specs/user_smoke.spec.js
diff --git a/docker/pleroma/entrypoint.e2e.sh b/docker/pleroma/entrypoint.e2e.sh
index 6945cf5b6..96920eeae 100644
--- a/docker/pleroma/entrypoint.e2e.sh
+++ b/docker/pleroma/entrypoint.e2e.sh
@@ -3,12 +3,26 @@
set -eu
SEED_SENTINEL_PATH="/var/lib/pleroma/.e2e_seeded"
+CONFIG_OVERRIDE_PATH="/var/lib/pleroma/config.exs"
echo "-- Waiting for database..."
while ! pg_isready -U "${DB_USER:-pleroma}" -d "postgres://${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-pleroma}" -t 1; do
sleep 1s
done
+echo "-- Writing E2E config overrides..."
+cat > "$CONFIG_OVERRIDE_PATH" <<'EOF'
+import Config
+
+config :pleroma, Pleroma.Captcha,
+ enabled: false
+
+config :pleroma, :instance,
+ registrations_open: true,
+ account_activation_required: false,
+ approval_required: false
+EOF
+
echo "-- Running migrations..."
/opt/pleroma/bin/pleroma_ctl migrate
diff --git a/src/modules/users.js b/src/modules/users.js
index 5563a280e..e9b56362d 100644
--- a/src/modules/users.js
+++ b/src/modules/users.js
@@ -551,7 +551,7 @@ const users = {
if (data.access_token) {
store.commit('signUpSuccess')
oauthStore.setToken(data.access_token)
- store.dispatch('loginUser', data.access_token)
+ await store.dispatch('loginUser', data.access_token)
return 'ok'
} else { // Request succeeded, but user cannot login yet.
store.commit('signUpNotice', data)
diff --git a/test/e2e-playwright/specs/user_smoke.spec.js b/test/e2e-playwright/specs/user_smoke.spec.js
new file mode 100644
index 000000000..080a20d1a
--- /dev/null
+++ b/test/e2e-playwright/specs/user_smoke.spec.js
@@ -0,0 +1,80 @@
+import { randomUUID } from 'node:crypto'
+import { test, expect } from 'playwright/test'
+
+const createTestUser = () => {
+ const id = randomUUID().slice(0, 8)
+ return {
+ username: `e2e_${id}`,
+ fullname: `E2E ${id}`,
+ email: `e2e_${id}@example.com`,
+ password: 'e2e-password'
+ }
+}
+
+const register = async (page, user) => {
+ await page.goto('/registration')
+
+ const registrationForm = page.locator('#main-scroller form.registration-form')
+ await registrationForm.locator('#sign-up-username').fill(user.username)
+ await registrationForm.locator('#sign-up-fullname').fill(user.fullname)
+ await registrationForm.locator('#email').fill(user.email)
+ await registrationForm.locator('#sign-up-password').fill(user.password)
+ await registrationForm.locator('#sign-up-password-confirmation').fill(user.password)
+ await Promise.all([
+ page.waitForURL(/\/main\/friends/),
+ registrationForm.getByRole('button', { name: 'Register' }).click()
+ ])
+}
+
+const logout = async (page) => {
+ await page.getByTitle('Log out').click()
+ const confirmLogout = page.getByRole('button', { name: 'Logout', exact: true })
+ if (await confirmLogout.isVisible()) {
+ await Promise.all([
+ page.waitForURL(/\/main\/(public|all)/),
+ confirmLogout.click()
+ ])
+ } else {
+ await page.waitForURL(/\/main\/(public|all)/)
+ }
+
+ await expect(page.locator('#sidebar form.login-form')).toBeVisible()
+}
+
+const login = async (page, user) => {
+ await page.goto('/login')
+
+ const loginForm = page.locator('#main-scroller form.login-form')
+ await loginForm.locator('#username').fill(user.username)
+ await loginForm.locator('#password').fill(user.password)
+ await loginForm.getByRole('button', { name: 'Log in' }).click()
+
+ await page.waitForURL(/\/main\/friends/)
+}
+
+test('user can register, log out, and log back in', async ({ page }) => {
+ const user = createTestUser()
+ await register(page, user)
+ await expect(page.getByTitle('Log out')).toBeVisible()
+
+ await logout(page)
+
+ await login(page, user)
+ await expect(page.getByTitle('Log out')).toBeVisible()
+})
+
+test('user can post a status', async ({ page }) => {
+ const user = createTestUser()
+ await register(page, user)
+
+ const statusText = `Hello from ${user.username} (${randomUUID().slice(0, 8)})`
+ const composer = page.locator('#sidebar .user-panel .post-status-form')
+ await composer.locator('textarea.form-post-body').fill(statusText)
+ await Promise.all([
+ page.waitForResponse((resp) => resp.request().method() === 'POST' && resp.url().includes('/api/v1/statuses') && resp.ok()),
+ composer.getByRole('button', { name: 'Post', exact: true }).click()
+ ])
+
+ await page.goto(`/users/${user.username}`)
+ await expect(page.getByText(statusText)).toBeVisible()
+})
diff --git a/tools/e2e/run.sh b/tools/e2e/run.sh
index 5ff2637ce..3c0ba8a36 100644
--- a/tools/e2e/run.sh
+++ b/tools/e2e/run.sh
@@ -24,10 +24,10 @@ result="$?"
set -e
if [ "$result" -ne 0 ]; then
+ docker compose -f "$COMPOSE_FILE" cp e2e:/app/test/e2e-playwright/test-results test/e2e-playwright >/dev/null 2>&1 || true
+ docker compose -f "$COMPOSE_FILE" cp e2e:/app/test/e2e-playwright/playwright-report test/e2e-playwright >/dev/null 2>&1 || true
mkdir -p test/e2e-playwright/test-results
docker compose -f "$COMPOSE_FILE" logs --no-color pleroma db > test/e2e-playwright/test-results/docker-compose.log 2>/dev/null || true
- docker compose -f "$COMPOSE_FILE" cp e2e:/app/test/e2e-playwright/test-results test/e2e-playwright/test-results >/dev/null 2>&1 || true
- docker compose -f "$COMPOSE_FILE" cp e2e:/app/test/e2e-playwright/playwright-report test/e2e-playwright/playwright-report >/dev/null 2>&1 || true
fi
cleanup
From 33c7876a8a27dff4fd2711bf6c5a9458422c2523 Mon Sep 17 00:00:00 2001
From: Lain Soykaf
Date: Wed, 7 Jan 2026 18:07:27 +0400
Subject: [PATCH 05/17] Add changelog
---
changelog.d/e2e-tests.add | 1 +
1 file changed, 1 insertion(+)
create mode 100644 changelog.d/e2e-tests.add
diff --git a/changelog.d/e2e-tests.add b/changelog.d/e2e-tests.add
new file mode 100644
index 000000000..ba62b25ac
--- /dev/null
+++ b/changelog.d/e2e-tests.add
@@ -0,0 +1 @@
+Add playwright E2E-tests with an optional docker-based backend
From 0492a8d6a057b8a82f668bf466dd6c13b23b0128 Mon Sep 17 00:00:00 2001
From: Henry Jameson
Date: Wed, 7 Jan 2026 16:45:10 +0200
Subject: [PATCH 06/17] fix actor type not setting
---
changelog.d/actor-type.fix | 1 +
src/components/user_card/user_card.js | 6 ++++--
src/components/user_card/user_card.vue | 2 +-
3 files changed, 6 insertions(+), 3 deletions(-)
create mode 100644 changelog.d/actor-type.fix
diff --git a/changelog.d/actor-type.fix b/changelog.d/actor-type.fix
new file mode 100644
index 000000000..a2c873c1a
--- /dev/null
+++ b/changelog.d/actor-type.fix
@@ -0,0 +1 @@
+fixed being unable to set actor type from profile page
diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js
index 7daee4b76..5e387d38b 100644
--- a/src/components/user_card/user_card.js
+++ b/src/components/user_card/user_card.js
@@ -122,6 +122,8 @@ export default {
data () {
const user = this.$store.getters.findUser(this.userId)
+ console.log('LOL', JSON.parse(JSON.stringify(user)))
+
return {
followRequestInProgress: false,
muteExpiryAmount: 0,
@@ -466,8 +468,8 @@ export default {
show_birthday: !!this.newShowBirthday,
}
- if (this.actorType) {
- params.actor_type = this.actorType
+ if (this.newActorType) {
+ params.actor_type = this.newActorType
}
if (this.newAvatarFile !== null) {
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index d6edc021f..e3b1b0175 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -208,7 +208,7 @@
{{ $t('user_card.group') }}
From d2f528bb154d56b61975b9a827e44fec3db56652 Mon Sep 17 00:00:00 2001
From: Lain Soykaf
Date: Wed, 7 Jan 2026 20:11:16 +0400
Subject: [PATCH 07/17] CI: run e2e job without docker-in-docker
Use GitLab services for Postgres + Pleroma and run Playwright tests directly in the Playwright image (works with gitlab-ci-local too).
---
.gitignore | 1 +
.gitlab-ci.yml | 129 +++++++++++++++++++++++++++++++++++++++++++------
2 files changed, 115 insertions(+), 15 deletions(-)
diff --git a/.gitignore b/.gitignore
index 085805753..c4a96ee1e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,6 +8,7 @@ test/e2e-playwright/test-results
test/e2e-playwright/playwright-report
selenium-debug.log
.idea/
+.gitlab-ci-local/
config/local.json
src/assets/emoji.json
logs/
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 399b9c644..eb1ab0d31 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -62,27 +62,126 @@ test:
e2e-pleroma:
stage: test
- image: docker:26
+ image: mcr.microsoft.com/playwright:v1.55.0-jammy
services:
- - name: docker:26-dind
+ - name: postgres:15-alpine
+ alias: db
+ - name: $PLEROMA_IMAGE
+ alias: pleroma
+ entrypoint: ["/bin/ash", "-c"]
+ command:
+ - |
+ set -eu
+
+ SEED_SENTINEL_PATH=/var/lib/pleroma/.e2e_seeded
+ CONFIG_OVERRIDE_PATH=/var/lib/pleroma/config.exs
+
+ echo '-- Waiting for database...'
+ while ! pg_isready -U ${DB_USER:-pleroma} -d postgres://${DB_HOST:-db}:${DB_PORT:-5432}/${DB_NAME:-pleroma} -t 1; do
+ sleep 1s
+ done
+
+ echo '-- Writing E2E config overrides...'
+ cat > $CONFIG_OVERRIDE_PATH </dev/null; then
+ kill -TERM $PLEROMA_PID
+ wait $PLEROMA_PID || true
+ fi
+ }
+
+ trap cleanup INT TERM
+
+ echo '-- Waiting for API...'
+ api_ok=false
+ for _i in $(seq 1 120); do
+ if wget -qO- http://127.0.0.1:4000/api/v1/instance >/dev/null 2>&1; then
+ api_ok=true
+ break
+ fi
+ sleep 1s
+ done
+
+ if [ $api_ok != true ]; then
+ echo 'Timed out waiting for Pleroma API to become available'
+ exit 1
+ fi
+
+ if [ ! -f $SEED_SENTINEL_PATH ]; then
+ if [ -n ${E2E_ADMIN_USERNAME:-} ] && [ -n ${E2E_ADMIN_PASSWORD:-} ] && [ -n ${E2E_ADMIN_EMAIL:-} ]; then
+ echo '-- Seeding admin user' $E2E_ADMIN_USERNAME '...'
+ if ! /opt/pleroma/bin/pleroma_ctl user new $E2E_ADMIN_USERNAME $E2E_ADMIN_EMAIL --admin --password $E2E_ADMIN_PASSWORD -y; then
+ echo '-- User already exists or creation failed, ensuring admin + confirmed...'
+ /opt/pleroma/bin/pleroma_ctl user set $E2E_ADMIN_USERNAME --admin --confirmed
+ fi
+ else
+ echo '-- Skipping admin seeding (missing E2E_ADMIN_* env)'
+ fi
+
+ touch $SEED_SENTINEL_PATH
+ fi
+
+ wait $PLEROMA_PID
tags:
- amd64
- himem
variables:
- DOCKER_HOST: tcp://docker:2375
- DOCKER_TLS_CERTDIR: ""
- DOCKER_DRIVER: overlay2
- COMPOSE_MENU: "false"
+ PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1"
+ PLEROMA_IMAGE: git.pleroma.social:5050/pleroma/pleroma:stable
+ POSTGRES_USER: pleroma
+ POSTGRES_PASSWORD: pleroma
+ POSTGRES_DB: pleroma
+ DB_USER: pleroma
+ DB_PASS: pleroma
+ DB_NAME: pleroma
+ DB_HOST: db
+ DB_PORT: 5432
+ DOMAIN: localhost
+ INSTANCE_NAME: Pleroma E2E
+ E2E_ADMIN_USERNAME: admin
+ E2E_ADMIN_PASSWORD: adminadmin
+ E2E_ADMIN_EMAIL: admin@example.com
+ ADMIN_EMAIL: $E2E_ADMIN_EMAIL
+ NOTIFY_EMAIL: $E2E_ADMIN_EMAIL
+ VITE_PROXY_TARGET: http://pleroma:4000
+ VITE_PROXY_ORIGIN: http://localhost:4000
+ E2E_BASE_URL: http://localhost:8080
script:
- - docker version
- - docker compose version
- - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY"
- - export PLEROMA_IMAGE="${PLEROMA_IMAGE:-$CI_REGISTRY/pleroma/pleroma:stable}"
- - docker compose -f docker-compose.e2e.yml up --build --remove-orphans --attach e2e --no-log-prefix --abort-on-container-exit --exit-code-from e2e
- after_script:
- - docker compose -f docker-compose.e2e.yml cp e2e:/app/test/e2e-playwright/test-results test/e2e-playwright/test-results || true
- - docker compose -f docker-compose.e2e.yml cp e2e:/app/test/e2e-playwright/playwright-report test/e2e-playwright/playwright-report || true
- - docker compose -f docker-compose.e2e.yml down -v --remove-orphans || true
+ - npm install -g yarn@1.22.22
+ - yarn --frozen-lockfile
+ - |
+ echo "-- Waiting for Pleroma API..."
+ api_ok="false"
+ for _i in $(seq 1 120); do
+ if wget -qO- http://pleroma:4000/api/v1/instance >/dev/null 2>&1; then
+ api_ok="true"
+ break
+ fi
+ sleep 1s
+ done
+ if [ "$api_ok" != "true" ]; then
+ echo "Timed out waiting for Pleroma API to become available"
+ exit 1
+ fi
+ - yarn e2e:pw
artifacts:
when: on_failure
paths:
From 03b6178d17614c9b38c970cee219a0a52525f5ee Mon Sep 17 00:00:00 2001
From: Henry Jameson
Date: Wed, 7 Jan 2026 21:01:34 +0200
Subject: [PATCH 08/17] fix missing string
---
src/i18n/en.json | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/i18n/en.json b/src/i18n/en.json
index 05a2d5ae5..099a31f64 100644
--- a/src/i18n/en.json
+++ b/src/i18n/en.json
@@ -1684,6 +1684,7 @@
"muted": "Muted",
"mute_confirm_title": "Mute confirmation",
"mute_confirm": "Do you really want to mute {user}?",
+ "mute_domain_confirm": "Do you really want to mute entire {domain}?",
"mute_confirm_accept_button": "Mute",
"mute_confirm_cancel_button": "Do not mute",
"mute_or": "or",
From 949aa90faaf7fa1a00714dd0cf1e013efb853c3b Mon Sep 17 00:00:00 2001
From: Henry Jameson
Date: Wed, 7 Jan 2026 21:09:01 +0200
Subject: [PATCH 09/17] fixed being unable to mute/unmute domains from status
context menu
---
src/components/confirm_modal/mute_confirm.js | 8 ++++----
.../status_action_buttons/action_button_container.js | 2 +-
2 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/src/components/confirm_modal/mute_confirm.js b/src/components/confirm_modal/mute_confirm.js
index a279dc716..b7aa5a68b 100644
--- a/src/components/confirm_modal/mute_confirm.js
+++ b/src/components/confirm_modal/mute_confirm.js
@@ -19,9 +19,9 @@ export default {
},
keypath () {
if (this.type === 'domain') {
- return 'status.mute_domain_confirm'
+ return 'user_card.mute_domain_confirm'
} else if (this.type === 'conversation') {
- return 'status.mute_conversation_confirm'
+ return 'user_card.mute_conversation_confirm'
}
},
conversationIsMuted () {
@@ -62,9 +62,9 @@ export default {
switch (this.type) {
case 'domain': {
if (!this.domainIsMuted) {
- this.$store.dispatch('muteDomain', { id: this.domain })
+ this.$store.dispatch('muteDomain', this.domain)
} else {
- this.$store.dispatch('unmuteDomain', { id: this.domain })
+ this.$store.dispatch('unmuteDomain', this.domain)
}
break
}
diff --git a/src/components/status_action_buttons/action_button_container.js b/src/components/status_action_buttons/action_button_container.js
index a8f20800b..5ea8db013 100644
--- a/src/components/status_action_buttons/action_button_container.js
+++ b/src/components/status_action_buttons/action_button_container.js
@@ -65,7 +65,7 @@ export default {
return this.$store.dispatch('unmuteConversation', { id: this.status.id })
},
unmuteDomain () {
- return this.$store.dispatch('unmuteDomain', this.user.id)
+ return this.$store.dispatch('unmuteDomain', this.domain)
},
toggleUserMute () {
if (this.userIsMuted) {
From 0dc8305e95412426d6123f4361c328ff5dfcee94 Mon Sep 17 00:00:00 2001
From: Henry Jameson
Date: Wed, 7 Jan 2026 23:15:23 +0200
Subject: [PATCH 10/17] don't display muted label on profile since backend
doesn't work this way improve display logic for mute/block cards
---
src/components/block_card/block_card.js | 4 ++--
src/components/mute_card/mute_card.js | 4 ++--
src/components/user_card/user_card.js | 10 +++-------
src/components/user_card/user_card.vue | 12 ------------
.../entity_normalizer/entity_normalizer.service.js | 10 ++++++++--
5 files changed, 15 insertions(+), 25 deletions(-)
diff --git a/src/components/block_card/block_card.js b/src/components/block_card/block_card.js
index 9a618db3f..2d4b17ef8 100644
--- a/src/components/block_card/block_card.js
+++ b/src/components/block_card/block_card.js
@@ -15,10 +15,10 @@ const BlockCard = {
return this.relationship.blocking
},
blockExpiryAvailable () {
- return this.user.block_expires_at !== undefined
+ return Object.hasOwn(this.user, 'block_expires_at')
},
blockExpiry () {
- return this.user.block_expires_at == null
+ return this.user.block_expires_at === false
? this.$t('user_card.block_expires_forever')
: this.$t('user_card.block_expires_at', [new Date(this.user.mute_expires_at).toLocaleString()])
},
diff --git a/src/components/mute_card/mute_card.js b/src/components/mute_card/mute_card.js
index 895586888..592df8dfe 100644
--- a/src/components/mute_card/mute_card.js
+++ b/src/components/mute_card/mute_card.js
@@ -14,10 +14,10 @@ const MuteCard = {
return this.relationship.muting
},
muteExpiryAvailable () {
- return this.user.mute_expires_at !== undefined
+ return Object.hasOwn(this.user, 'mute_expires_at')
},
muteExpiry () {
- return this.user.mute_expires_at == null
+ return this.user.mute_expires_at === false
? this.$t('user_card.mute_expires_forever')
: this.$t('user_card.mute_expires_at', [new Date(this.user.mute_expires_at).toLocaleString()])
}
diff --git a/src/components/user_card/user_card.js b/src/components/user_card/user_card.js
index 5e387d38b..ccc03b414 100644
--- a/src/components/user_card/user_card.js
+++ b/src/components/user_card/user_card.js
@@ -122,12 +122,8 @@ export default {
data () {
const user = this.$store.getters.findUser(this.userId)
- console.log('LOL', JSON.parse(JSON.stringify(user)))
-
return {
followRequestInProgress: false,
- muteExpiryAmount: 0,
- muteExpiryUnit: 'minutes',
// Editable stuff
editImage: false,
@@ -261,15 +257,15 @@ export default {
return 'note' in this.relationship
},
muteExpiryAvailable () {
- return this.user.mute_expires_at !== undefined
+ return Object.hasOwn(this.user, 'mute_expires_at')
},
muteExpiry () {
- return this.user.mute_expires_at == null
+ return this.user.mute_expires_at === false
? this.$t('user_card.mute_expires_forever')
: this.$t('user_card.mute_expires_at', [new Date(this.user.mute_expires_at).toLocaleString()])
},
blockExpiryAvailable () {
- return this.user.block_expires_at !== undefined
+ return Object.hasOwn(this.user, 'block_expires_at')
},
blockExpiry () {
return this.user.block_expires_at == null
diff --git a/src/components/user_card/user_card.vue b/src/components/user_card/user_card.vue
index e3b1b0175..66529f59b 100644
--- a/src/components/user_card/user_card.vue
+++ b/src/components/user_card/user_card.vue
@@ -212,18 +212,6 @@
>
{{ $t('user_card.group') }}
-
- {{ muteExpiry }}
-
-
- {{ blockExpiry }}
-
diff --git a/src/services/entity_normalizer/entity_normalizer.service.js b/src/services/entity_normalizer/entity_normalizer.service.js
index 28f1bc2aa..36b42dd47 100644
--- a/src/services/entity_normalizer/entity_normalizer.service.js
+++ b/src/services/entity_normalizer/entity_normalizer.service.js
@@ -51,8 +51,14 @@ export const parseUser = (data) => {
output.screen_name = data.acct
output.fqn = data.fqn
output.statusnet_profile_url = data.url
- output.mute_expires_at = data.mute_expires_at
- output.block_expires_at = data.block_expires_at
+
+ if (Object.hasOwn(data, 'mute_expires_at')) {
+ output.mute_expires_at = data.mute_expires_at == null ? false : data.mute_expires_at
+ }
+
+ if (Object.hasOwn(data, 'block_expires_at')) {
+ output.block_expires_at = data.block_expires_at == null ? false : data.block_expires_at
+ }
// There's nothing else to get
if (mastoShort) {
From b8bfd6b1a9968001f2c8ce57b5bb5e4f46abb40b Mon Sep 17 00:00:00 2001
From: Lain Soykaf
Date: Thu, 8 Jan 2026 14:35:51 +0400
Subject: [PATCH 11/17] CI: enable per-build network for e2e
Fixes Pleroma service being unable to reach the Postgres service on runners that use legacy container links.
---
.gitlab-ci.yml | 1 +
1 file changed, 1 insertion(+)
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index eb1ab0d31..5045d8c68 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -145,6 +145,7 @@ e2e-pleroma:
- himem
variables:
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1"
+ FF_NETWORK_PER_BUILD: "true"
PLEROMA_IMAGE: git.pleroma.social:5050/pleroma/pleroma:stable
POSTGRES_USER: pleroma
POSTGRES_PASSWORD: pleroma
From 6fc8cc32cacc2a75a43015a988e5deb92585248e Mon Sep 17 00:00:00 2001
From: Lain Soykaf
Date: Thu, 8 Jan 2026 15:09:41 +0400
Subject: [PATCH 12/17] Test: exclude Playwright e2e specs from vitest
Vitest picks up *.spec.js under test/ by default; exclude test/e2e-playwright while preserving default excludes.
---
vite.config.js | 5 +++++
1 file changed, 5 insertions(+)
diff --git a/vite.config.js b/vite.config.js
index ca48ce3bf..4bba3da86 100644
--- a/vite.config.js
+++ b/vite.config.js
@@ -1,6 +1,7 @@
import { fileURLToPath } from 'node:url'
import { dirname, resolve } from 'node:path'
import { defineConfig } from 'vite'
+import { configDefaults } from 'vitest/config'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import stylelint from 'vite-plugin-stylelint'
@@ -217,6 +218,10 @@ export default defineConfig(async ({ mode, command }) => {
},
test: {
globals: true,
+ exclude: [
+ ...configDefaults.exclude,
+ 'test/e2e-playwright/**'
+ ],
browser: {
enabled: true,
provider: 'playwright',
From ca10ffca8d689a9ec7c61f06d68d27432f08f222 Mon Sep 17 00:00:00 2001
From: Henry Jameson
Date: Thu, 8 Jan 2026 17:04:15 +0200
Subject: [PATCH 13/17] proper playwright version
---
docker/e2e/Dockerfile.e2e | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/docker/e2e/Dockerfile.e2e b/docker/e2e/Dockerfile.e2e
index ec780e894..e84359ceb 100644
--- a/docker/e2e/Dockerfile.e2e
+++ b/docker/e2e/Dockerfile.e2e
@@ -1,4 +1,4 @@
-FROM mcr.microsoft.com/playwright:v1.55.0-jammy
+FROM mcr.microsoft.com/playwright:v1.57.0-jammy
WORKDIR /app
@@ -14,4 +14,3 @@ COPY . .
ENV CI=1
CMD ["yarn", "e2e:pw"]
-
From 3d78a34daae0d1f6d5aca481e91bffca46279251 Mon Sep 17 00:00:00 2001
From: Henry Jameson
Date: Thu, 8 Jan 2026 17:05:12 +0200
Subject: [PATCH 14/17] chlg
---
.gitlab-ci.yml | 2 +-
changelog.d/e2e.skip | 1 +
2 files changed, 2 insertions(+), 1 deletion(-)
create mode 100644 changelog.d/e2e.skip
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 5045d8c68..e277002b8 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -62,7 +62,7 @@ test:
e2e-pleroma:
stage: test
- image: mcr.microsoft.com/playwright:v1.55.0-jammy
+ image: mcr.microsoft.com/playwright:v1.57.0-jammy
services:
- name: postgres:15-alpine
alias: db
diff --git a/changelog.d/e2e.skip b/changelog.d/e2e.skip
new file mode 100644
index 000000000..e84c25121
--- /dev/null
+++ b/changelog.d/e2e.skip
@@ -0,0 +1 @@
+fix e2e
From 42930252b15127b4df463c8bfce5c5a8ae5a5749 Mon Sep 17 00:00:00 2001
From: Henry Jameson
Date: Thu, 8 Jan 2026 17:26:52 +0200
Subject: [PATCH 15/17] better imports organization
---
biome.json | 18 +++++-
build/emojis_plugin.js | 1 +
build/service_worker_messages.js | 1 +
build/sw_plugin.js | 1 +
src/App.js | 1 +
src/boot/routes.js | 1 +
.../account_actions/account_actions.js | 8 ++-
src/components/announcement/announcement.js | 3 +-
.../announcements_page/announcements_page.js | 3 +-
src/components/attachment/attachment.js | 16 +++---
src/components/auth_form/auth_form.js | 3 +-
.../bookmark_folders_menu_content.js | 1 +
src/components/chat/chat.js | 8 ++-
src/components/chat_list/chat_list.js | 1 +
.../chat_list_item/chat_list_item.js | 3 +-
src/components/chat_message/chat_message.js | 8 ++-
src/components/chat_new/chat_new.js | 6 +-
src/components/chat_title/chat_title.js | 3 +-
src/components/color_input/color_input.vue | 6 +-
.../component_preview/component_preview.js | 1 -
src/components/confirm_modal/mute_confirm.js | 3 +-
.../contrast_ratio/contrast_ratio.vue | 3 +-
src/components/conversation/conversation.js | 20 ++++---
src/components/desktop_nav/desktop_nav.js | 8 ++-
src/components/draft/draft.js | 6 +-
.../edit_status_modal/edit_status_modal.js | 1 +
src/components/emoji_input/emoji_input.js | 6 +-
src/components/emoji_picker/emoji_picker.js | 14 +++--
.../emoji_reactions/emoji_reactions.js | 5 +-
.../extra_notifications.js | 10 ++--
src/components/flash/flash.js | 3 +-
src/components/font_control/font_control.js | 9 +--
src/components/gallery/gallery.js | 1 +
.../global_notice_list/global_notice_list.js | 3 +-
src/components/image_cropper/image_cropper.js | 1 +
.../interface_language_switcher.js | 3 +-
src/components/lists_edit/lists_edit.js | 8 ++-
.../lists_menu/lists_menu_content.js | 3 +-
.../lists_user_search/lists_user_search.js | 6 +-
src/components/login_form/login_form.js | 8 ++-
src/components/media_modal/media_modal.js | 15 ++---
src/components/media_upload/media_upload.js | 5 +-
src/components/mention_link/mention_link.js | 8 ++-
src/components/mentions_line/mentions_line.js | 3 +-
src/components/mfa_form/recovery_form.js | 8 ++-
src/components/mfa_form/totp_form.js | 8 ++-
src/components/mobile_nav/mobile_nav.js | 29 +++++-----
.../mobile_post_status_button.js | 6 +-
.../moderation_tools/moderation_tools.js | 6 +-
src/components/nav_panel/nav_panel.js | 24 ++++----
src/components/navigation/navigation_entry.js | 8 ++-
src/components/navigation/navigation_pins.js | 30 +++++-----
src/components/notification/notification.js | 30 +++++-----
.../notifications/notification_filters.vue | 3 +-
src/components/notifications/notifications.js | 18 +++---
.../palette_editor/palette_editor.vue | 8 ++-
.../password_reset/password_reset.js | 6 +-
src/components/poll/poll.js | 1 +
src/components/poll/poll_form.js | 5 +-
.../post_status_form/post_status_form.js | 28 +++++-----
.../post_status_modal/post_status_modal.js | 1 +
.../quick_filter_settings.js | 10 ++--
.../quick_view_settings.js | 12 ++--
src/components/registration/registration.js | 3 +-
src/components/rich_content/rich_content.jsx | 1 +
src/components/search/search.js | 6 +-
.../settings_modal/admin_tabs/emoji_tab.js | 26 +++++----
.../admin_tabs/frontends_tab.js | 5 +-
.../settings_modal/admin_tabs/http_tab.js | 1 +
.../settings_modal/admin_tabs/instance_tab.js | 2 +-
.../settings_modal/admin_tabs/links_tab.js | 2 +-
.../admin_tabs/monitoring_tab.js | 5 +-
.../settings_modal/admin_tabs/rates_tab.js | 1 -
.../settings_modal/helpers/draft_buttons.vue | 3 +-
.../settings_modal/helpers/help_indicator.vue | 3 +-
.../helpers/modified_indicator.vue | 3 +-
.../helpers/profile_setting_indicator.vue | 3 +-
.../helpers/pwa_manifest_icons_setting.js | 1 +
.../settings_modal/helpers/setting.js | 1 +
.../helpers/vertical_tab_switcher.jsx | 4 +-
.../settings_modal/settings_modal.js | 18 +++---
.../settings_modal_admin_content.js | 39 ++++++-------
.../settings_modal_user_content.js | 35 ++++++------
.../settings_modal/tabs/appearance_tab.js | 1 +
.../settings_modal/tabs/clutter_tab.js | 8 +--
.../settings_modal/tabs/composing_tab.js | 30 +++++-----
.../tabs/data_import_export_tab.js | 3 +-
.../settings_modal/tabs/filtering_tab.js | 5 +-
.../settings_modal/tabs/general_tab.js | 4 +-
.../tabs/mutes_and_blocks_tab.js | 1 +
.../tabs/old_theme_tab/theme_preview.vue | 3 +-
.../settings_modal/tabs/posts_tab.js | 1 -
.../settings_modal/tabs/profile_tab.js | 11 ++--
.../settings_modal/tabs/security_tab/mfa.js | 1 +
.../tabs/security_tab/mfa_totp.js | 1 +
.../tabs/style_tab/style_tab.js | 34 +++++------
.../tabs/style_tab/virtual_directives_tab.js | 4 +-
.../shadow_control/shadow_control.js | 16 +++---
src/components/shout_panel/shout_panel.js | 5 +-
src/components/side_drawer/side_drawer.js | 20 ++++---
src/components/staff_panel/staff_panel.js | 1 +
src/components/status/status.js | 46 +++++++--------
.../status_action_buttons/action_button.js | 7 ++-
.../action_button_container.js | 9 +--
.../status_action_buttons.js | 6 +-
src/components/status_body/status_body.js | 8 ++-
.../status_bookmark_folder_menu.js | 6 +-
.../status_content/status_content.js | 16 +++---
.../status_history_modal.js | 1 +
.../status_popover/status_popover.js | 5 +-
.../still-image/still-image-emoji-popover.vue | 1 +
src/components/tab_switcher/tab_switcher.jsx | 4 +-
src/components/thread_tree/thread_tree.js | 3 +-
src/components/timeline/timeline.js | 22 ++++----
src/components/timeline_menu/timeline_menu.js | 8 ++-
.../update_notification.js | 5 +-
src/components/user_avatar/user_avatar.js | 5 +-
src/components/user_card/user_card.js | 56 ++++++++++---------
.../user_list_menu/user_list_menu.js | 6 +-
.../user_list_popover/user_list_popover.js | 8 ++-
src/components/user_panel/user_panel.js | 1 +
src/components/user_popover/user_popover.js | 1 +
src/components/user_profile/user_profile.js | 6 +-
.../who_to_follow_panel.js | 1 +
src/hocs/with_load_more/with_load_more.jsx | 1 +
.../with_subscription/with_subscription.jsx | 1 +
src/i18n/messages.js | 1 +
src/lib/persisted_state.js | 1 +
src/main.js | 1 +
src/modules/api.js | 1 +
src/modules/chats.js | 1 +
src/modules/config.js | 2 +-
src/modules/instance.js | 4 +-
src/modules/notifications.js | 1 -
src/modules/statuses.js | 1 +
src/modules/users.js | 1 +
src/services/api/api.service.js | 1 +
.../entity_normalizer.service.js | 1 +
.../html_tree_converter.service.js | 1 +
src/services/locale/locale.service.js | 1 +
src/services/new_api/oauth.js | 1 +
src/services/poll/poll.service.js | 1 +
.../status_poster/status_poster.service.js | 1 +
src/services/style_setter/style_setter.js | 1 +
src/services/theme_data/pleromafe.js | 1 +
src/services/theme_data/theme2_to_theme3.js | 1 +
.../theme_data/theme3_slot_functions.js | 1 +
src/services/theme_data/theme_data.service.js | 1 +
.../theme_data/theme_data_3.service.js | 2 +-
.../timeline_fetcher.service.js | 1 +
src/stores/auth_flow.js | 1 +
src/stores/media_viewer.js | 1 +
src/stores/oauth.js | 1 +
src/stores/reports.js | 1 +
src/stores/serverSideStorage.js | 3 +-
src/sw.js | 13 +++--
test/fixtures/mock_store.js | 3 +-
test/fixtures/setup_test.js | 3 +-
test/unit/specs/boot/routes.spec.js | 3 +-
test/unit/specs/components/draft.spec.js | 3 +-
.../unit/specs/components/emoji_input.spec.js | 3 +-
.../specs/components/rich_content.spec.js | 1 +
.../specs/components/user_profile.spec.js | 3 +-
test/unit/specs/lib/persisted_state.spec.js | 3 +-
test/unit/specs/stores/lists.spec.js | 3 +-
test/unit/specs/stores/oauth.spec.js | 3 +-
vite.config.js | 1 +
167 files changed, 663 insertions(+), 445 deletions(-)
diff --git a/biome.json b/biome.json
index 9b4ee2663..d64639d52 100644
--- a/biome.json
+++ b/biome.json
@@ -124,6 +124,22 @@
],
"assist": {
"enabled": true,
- "actions": { "source": { "organizeImports": "on" } }
+ "actions": {
+ "source": {
+ "organizeImports": {
+ "level": "on",
+ "options": {
+ "groups": [
+ [":NODE:", ":PACKAGE:", "!src/**", "!@fortawesome/**"],
+ ":BLANK_LINE:",
+ [":PATH:", "src/**"],
+ ":BLANK_LINE:",
+ "@fortawesome/fontawesome-svg-core",
+ "@fortawesome/*"
+ ]
+ }
+ }
+ }
+ }
}
}
diff --git a/build/emojis_plugin.js b/build/emojis_plugin.js
index 43a665e50..7979086dd 100644
--- a/build/emojis_plugin.js
+++ b/build/emojis_plugin.js
@@ -1,5 +1,6 @@
import { access } from 'node:fs/promises'
import { resolve } from 'node:path'
+
import { languages } from '../src/i18n/languages.js'
const annotationsImportPrefix = '@kazvmoe-infra/unicode-emoji-json/annotations/'
diff --git a/build/service_worker_messages.js b/build/service_worker_messages.js
index 0ebd2b471..0948aa919 100644
--- a/build/service_worker_messages.js
+++ b/build/service_worker_messages.js
@@ -1,6 +1,7 @@
import { readFile } from 'node:fs/promises'
import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
+
import { langCodeToJsonName, languages } from '../src/i18n/languages.js'
const i18nDir = resolve(
diff --git a/build/sw_plugin.js b/build/sw_plugin.js
index 88520ba31..03c5978d7 100644
--- a/build/sw_plugin.js
+++ b/build/sw_plugin.js
@@ -3,6 +3,7 @@ import { dirname, resolve } from 'node:path'
import { fileURLToPath } from 'node:url'
import * as esbuild from 'esbuild'
import { build } from 'vite'
+
import {
generateServiceWorkerMessages,
i18nFiles,
diff --git a/src/App.js b/src/App.js
index 83e7f8ee5..33645c63d 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,6 +1,7 @@
import { throttle } from 'lodash'
import { defineAsyncComponent } from 'vue'
import { mapGetters } from 'vuex'
+
import DesktopNav from './components/desktop_nav/desktop_nav.vue'
import EditStatusModal from './components/edit_status_modal/edit_status_modal.vue'
import FeaturesPanel from './components/features_panel/features_panel.vue'
diff --git a/src/boot/routes.js b/src/boot/routes.js
index c601531a9..3296755a1 100644
--- a/src/boot/routes.js
+++ b/src/boot/routes.js
@@ -26,6 +26,7 @@ import ShoutPanel from 'components/shout_panel/shout_panel.vue'
import TagTimeline from 'components/tag_timeline/tag_timeline.vue'
import UserProfile from 'components/user_profile/user_profile.vue'
import WhoToFollow from 'components/who_to_follow/who_to_follow.vue'
+
import NavPanel from 'src/components/nav_panel/nav_panel.vue'
import BookmarkFolderEdit from '../components/bookmark_folder_edit/bookmark_folder_edit.vue'
import BookmarkFolders from '../components/bookmark_folders/bookmark_folders.vue'
diff --git a/src/components/account_actions/account_actions.js b/src/components/account_actions/account_actions.js
index 184c2b623..ee94dc544 100644
--- a/src/components/account_actions/account_actions.js
+++ b/src/components/account_actions/account_actions.js
@@ -1,13 +1,15 @@
-import { library } from '@fortawesome/fontawesome-svg-core'
-import { faEllipsisV } from '@fortawesome/free-solid-svg-icons'
+import { mapState } from 'vuex'
+
import UserListMenu from 'src/components/user_list_menu/user_list_menu.vue'
import UserTimedFilterModal from 'src/components/user_timed_filter_modal/user_timed_filter_modal.vue'
import { useReportsStore } from 'src/stores/reports'
-import { mapState } from 'vuex'
import ConfirmModal from '../confirm_modal/confirm_modal.vue'
import Popover from '../popover/popover.vue'
import ProgressButton from '../progress_button/progress_button.vue'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import { faEllipsisV } from '@fortawesome/free-solid-svg-icons'
+
library.add(faEllipsisV)
const AccountActions = {
diff --git a/src/components/announcement/announcement.js b/src/components/announcement/announcement.js
index 356852cff..906b84ce2 100644
--- a/src/components/announcement/announcement.js
+++ b/src/components/announcement/announcement.js
@@ -1,5 +1,6 @@
-import { useAnnouncementsStore } from 'src/stores/announcements'
import { mapState } from 'vuex'
+
+import { useAnnouncementsStore } from 'src/stores/announcements'
import localeService from '../../services/locale/locale.service.js'
import AnnouncementEditor from '../announcement_editor/announcement_editor.vue'
import RichContent from '../rich_content/rich_content.jsx'
diff --git a/src/components/announcements_page/announcements_page.js b/src/components/announcements_page/announcements_page.js
index d3429613c..0c4383d2e 100644
--- a/src/components/announcements_page/announcements_page.js
+++ b/src/components/announcements_page/announcements_page.js
@@ -1,5 +1,6 @@
-import { useAnnouncementsStore } from 'src/stores/announcements'
import { mapState } from 'vuex'
+
+import { useAnnouncementsStore } from 'src/stores/announcements'
import Announcement from '../announcement/announcement.vue'
import AnnouncementEditor from '../announcement_editor/announcement_editor.vue'
diff --git a/src/components/attachment/attachment.js b/src/components/attachment/attachment.js
index 8c7700c8a..31fceba60 100644
--- a/src/components/attachment/attachment.js
+++ b/src/components/attachment/attachment.js
@@ -1,3 +1,12 @@
+import { mapGetters } from 'vuex'
+
+import { useMediaViewerStore } from 'src/stores/media_viewer'
+import nsfwImage from '../../assets/nsfw.png'
+import fileTypeService from '../../services/file_type/file_type.service.js'
+import Flash from '../flash/flash.vue'
+import StillImage from '../still-image/still-image.vue'
+import VideoAttachment from '../video_attachment/video_attachment.vue'
+
import { library } from '@fortawesome/fontawesome-svg-core'
import {
faAlignRight,
@@ -12,13 +21,6 @@ import {
faTrashAlt,
faVideo,
} from '@fortawesome/free-solid-svg-icons'
-import { useMediaViewerStore } from 'src/stores/media_viewer'
-import { mapGetters } from 'vuex'
-import nsfwImage from '../../assets/nsfw.png'
-import fileTypeService from '../../services/file_type/file_type.service.js'
-import Flash from '../flash/flash.vue'
-import StillImage from '../still-image/still-image.vue'
-import VideoAttachment from '../video_attachment/video_attachment.vue'
library.add(
faFile,
diff --git a/src/components/auth_form/auth_form.js b/src/components/auth_form/auth_form.js
index 1e9b86c57..ce88aa6f9 100644
--- a/src/components/auth_form/auth_form.js
+++ b/src/components/auth_form/auth_form.js
@@ -1,6 +1,7 @@
import { mapState } from 'pinia'
-import { useAuthFlowStore } from 'src/stores/auth_flow'
import { h, resolveComponent } from 'vue'
+
+import { useAuthFlowStore } from 'src/stores/auth_flow'
import LoginForm from '../login_form/login_form.vue'
import MFARecoveryForm from '../mfa_form/recovery_form.vue'
import MFATOTPForm from '../mfa_form/totp_form.vue'
diff --git a/src/components/bookmark_folders_menu/bookmark_folders_menu_content.js b/src/components/bookmark_folders_menu/bookmark_folders_menu_content.js
index cc8135334..e84b3bc85 100644
--- a/src/components/bookmark_folders_menu/bookmark_folders_menu_content.js
+++ b/src/components/bookmark_folders_menu/bookmark_folders_menu_content.js
@@ -1,4 +1,5 @@
import { mapState } from 'pinia'
+
import { getBookmarkFolderEntries } from 'src/components/navigation/filter.js'
import NavigationEntry from 'src/components/navigation/navigation_entry.vue'
import { useBookmarkFoldersStore } from 'src/stores/bookmark_folders'
diff --git a/src/components/chat/chat.js b/src/components/chat/chat.js
index 2a0d91761..6dd69ada4 100644
--- a/src/components/chat/chat.js
+++ b/src/components/chat/chat.js
@@ -1,9 +1,8 @@
-import { library } from '@fortawesome/fontawesome-svg-core'
-import { faChevronDown, faChevronLeft } from '@fortawesome/free-solid-svg-icons'
import _ from 'lodash'
import { mapState as mapPiniaState } from 'pinia'
-import { useInterfaceStore } from 'src/stores/interface.js'
import { mapGetters, mapState } from 'vuex'
+
+import { useInterfaceStore } from 'src/stores/interface.js'
import { WSConnectionStatus } from '../../services/api/api.service.js'
import chatService from '../../services/chat_service/chat_service.js'
import { buildFakeMessage } from '../../services/chat_utils/chat_utils.js'
@@ -18,6 +17,9 @@ import {
isScrollable,
} from './chat_layout_utils.js'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import { faChevronDown, faChevronLeft } from '@fortawesome/free-solid-svg-icons'
+
library.add(faChevronDown, faChevronLeft)
const BOTTOMED_OUT_OFFSET = 10
diff --git a/src/components/chat_list/chat_list.js b/src/components/chat_list/chat_list.js
index 8081d0670..4c3194ae1 100644
--- a/src/components/chat_list/chat_list.js
+++ b/src/components/chat_list/chat_list.js
@@ -1,4 +1,5 @@
import { mapGetters, mapState } from 'vuex'
+
import ChatListItem from '../chat_list_item/chat_list_item.vue'
import ChatNew from '../chat_new/chat_new.vue'
import List from '../list/list.vue'
diff --git a/src/components/chat_list_item/chat_list_item.js b/src/components/chat_list_item/chat_list_item.js
index ba8f5528b..0923a8568 100644
--- a/src/components/chat_list_item/chat_list_item.js
+++ b/src/components/chat_list_item/chat_list_item.js
@@ -1,5 +1,6 @@
-import fileType from 'src/services/file_type/file_type.service'
import { mapState } from 'vuex'
+
+import fileType from 'src/services/file_type/file_type.service'
import AvatarList from '../avatar_list/avatar_list.vue'
import ChatTitle from '../chat_title/chat_title.vue'
import StatusBody from '../status_content/status_content.vue'
diff --git a/src/components/chat_message/chat_message.js b/src/components/chat_message/chat_message.js
index 0d3876ab6..f3cc495c2 100644
--- a/src/components/chat_message/chat_message.js
+++ b/src/components/chat_message/chat_message.js
@@ -1,9 +1,8 @@
-import { library } from '@fortawesome/fontawesome-svg-core'
-import { faEllipsisH, faTimes } from '@fortawesome/free-solid-svg-icons'
import { mapState as mapPiniaState } from 'pinia'
-import { useInterfaceStore } from 'src/stores/interface'
import { defineAsyncComponent } from 'vue'
import { mapGetters, mapState } from 'vuex'
+
+import { useInterfaceStore } from 'src/stores/interface'
import Attachment from '../attachment/attachment.vue'
import ChatMessageDate from '../chat_message_date/chat_message_date.vue'
import Gallery from '../gallery/gallery.vue'
@@ -12,6 +11,9 @@ import Popover from '../popover/popover.vue'
import StatusContent from '../status_content/status_content.vue'
import UserAvatar from '../user_avatar/user_avatar.vue'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import { faEllipsisH, faTimes } from '@fortawesome/free-solid-svg-icons'
+
library.add(faTimes, faEllipsisH)
const ChatMessage = {
diff --git a/src/components/chat_new/chat_new.js b/src/components/chat_new/chat_new.js
index d9f73193c..50e0f7a8f 100644
--- a/src/components/chat_new/chat_new.js
+++ b/src/components/chat_new/chat_new.js
@@ -1,9 +1,11 @@
-import { library } from '@fortawesome/fontawesome-svg-core'
-import { faChevronLeft, faSearch } from '@fortawesome/free-solid-svg-icons'
import { mapGetters, mapState } from 'vuex'
+
import BasicUserCard from '../basic_user_card/basic_user_card.vue'
import UserAvatar from '../user_avatar/user_avatar.vue'
+import { library } from '@fortawesome/fontawesome-svg-core'
+import { faChevronLeft, faSearch } from '@fortawesome/free-solid-svg-icons'
+
library.add(faSearch, faChevronLeft)
const chatNew = {
diff --git a/src/components/chat_title/chat_title.js b/src/components/chat_title/chat_title.js
index 7748f8973..3447f5163 100644
--- a/src/components/chat_title/chat_title.js
+++ b/src/components/chat_title/chat_title.js
@@ -1,5 +1,6 @@
-import RichContent from 'src/components/rich_content/rich_content.jsx'
import { defineAsyncComponent } from 'vue'
+
+import RichContent from 'src/components/rich_content/rich_content.jsx'
import UserAvatar from '../user_avatar/user_avatar.vue'
export default {
diff --git a/src/components/color_input/color_input.vue b/src/components/color_input/color_input.vue
index 76a00416c..a6fc2047b 100644
--- a/src/components/color_input/color_input.vue
+++ b/src/components/color_input/color_input.vue
@@ -64,12 +64,14 @@