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') }} - - - - -
- {{ $t('admin_dash.rate_limit.authenticated') }} - - - - -
  + {{ $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') }} + + + + +
+ {{ $t('admin_dash.rate_limit.authenticated') }} + + + + +
{ + 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 @@