Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
136 changes: 136 additions & 0 deletions .github/workflows/e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# The browser tests: a throwaway Nextcloud with this app installed, driven by
# Playwright the way a person would drive it. The unit and integration suites
# cannot see a page that stays empty; this can. See tests/e2e/.

name: Browser tests

on:
pull_request:
push:
branches:
- master

permissions:
contents: read

concurrency:
group: e2e-${{ github.head_ref || github.run_id }}
cancel-in-progress: true

jobs:
e2e:
runs-on: ubuntu-latest
name: Playwright against Nextcloud stable35

steps:
- name: Set app env
run: echo "APP_NAME=${GITHUB_REPOSITORY##*/}" >> $GITHUB_ENV

- name: Checkout server
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
submodules: true
repository: nextcloud/server
ref: stable35

- name: Checkout app
uses: actions/checkout@692973e3d937129bcbf40652eb9f2f61becf3332 # v4.1.7
with:
path: apps/${{ env.APP_NAME }}

- name: Set up php
uses: shivammathur/setup-php@c541c155eee45413f5b09a52248675b1a2575231 # v2.31.1
with:
php-version: '8.3'
extensions: bz2, ctype, curl, dom, fileinfo, gd, iconv, intl, json, libxml, mbstring, openssl, pcntl, posix, session, simplexml, xmlreader, xmlwriter, zip, zlib, sqlite, pdo_sqlite
coverage: none
ini-file: development
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

- name: Set up the app's PHP dependencies
working-directory: apps/${{ env.APP_NAME }}
run: composer i --no-dev

- name: Set up Nextcloud
env:
OC_PASS: Browser-Test-2026!
run: |
mkdir data
./occ maintenance:install --verbose --database=sqlite --database-name=nextcloud --admin-user admin --admin-pass admin
./occ app:enable --force ${{ env.APP_NAME }}
# a second account and a group, so the group list has somebody in it
./occ user:add --password-from-env --display-name "Bob" bob
./occ group:add design
./occ group:adduser design admin
./occ group:adduser design bob
./occ config:system:set trusted_domains 1 --value localhost:8080
./occ config:system:set overwrite.cli.url --value http://localhost:8080
# the app's own idea of where it lives; the setup page writes these on
# a real instance, and everything from actor creation to a timeline
# read throws without them (tests/Integration/bootstrap.php does the same)
./occ config:app:set social cloud_url --value http://localhost:8080
./occ config:app:set social social_url --value http://localhost:8080/index.php/apps/social/
# the instance has to be allowed to talk to itself: with the built-in
# server it is `localhost`, which the HTTP client refuses by default
./occ config:system:set allow_local_remote_servers --value true --type boolean

- name: Run Nextcloud
# several workers, not the default one: rendering the app makes the
# server fetch its own avatar route over HTTP, and a single-worker
# built-in server deadlocks on that until the ten-second timeout, so
# every page came out as a 500
run: PHP_CLI_SERVER_WORKERS=8 php -S localhost:8080 &

- name: Read package.json node and npm engines version
uses: skjnldsv/read-package-engines-version-actions@06d6baf7d8f41934ab630e97d9e6c0bc9c9ac5e4 # v3
id: versions
with:
path: apps/${{ env.APP_NAME }}
fallbackNode: '^20'
fallbackNpm: '^10'

- name: Set up node ${{ steps.versions.outputs.nodeVersion }}
uses: actions/setup-node@1e60f620b9541d16bece96c5465dc8ee9832be0b # v4.0.3
with:
node-version: ${{ steps.versions.outputs.nodeVersion }}

- name: Set up npm ${{ steps.versions.outputs.npmVersion }}
run: npm i -g 'npm@${{ steps.versions.outputs.npmVersion }}'

- name: Install the test runner
working-directory: apps/${{ env.APP_NAME }}
run: |
npm ci
npx playwright install --with-deps chromium

- name: Browser tests
working-directory: apps/${{ env.APP_NAME }}
env:
E2E_BASE_URL: http://localhost:8080
# not the admin: for an admin the app first runs its setup check, and
# the WebFinger probe cannot pass behind PHP's built-in server (no
# .well-known rewrite), so the admin would see the setup page and no
# sidebar. A plain account gets the app.
E2E_USER: bob
E2E_PASSWORD: Browser-Test-2026!
E2E_GROUP: design
run: npm run test:e2e

- name: Keep the report when something failed
if: failure()
uses: actions/upload-artifact@50769540e7f4bd5e21e526ee35c689e35e0d6874 # v4.4.0
with:
name: playwright-report
# relative to the config file, tests/e2e/playwright.config.mjs
path: |
apps/${{ env.APP_NAME }}/tests/e2e/playwright-report
apps/${{ env.APP_NAME }}/tests/e2e/test-results
retention-days: 7

- name: Print the server log
if: failure()
run: cat data/nextcloud.log
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,6 @@ RELEASE.md
# test artefacts
tests/.phpunit.result.cache
coverage/
# Playwright
playwright-report/
test-results/
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,23 @@ NEXTCLOUD_ROOT=/path/to/nextcloud composer run test:integration

See `tests/Integration/README.md` for what it covers.

### Browser tests

`tests/e2e/` drives a real Nextcloud with the app installed, in Chromium,
through [Playwright](https://playwright.dev): sign in, open the app, write a
post and see it in the feed, walk the Discover page, switch scopes, open a
group list. Nothing is mocked, and the bundle under test is the committed one
in `js/`. `.github/workflows/e2e.yml` sets up a throwaway server for every pull
request; against an instance of your own:

```
npx playwright install chromium
E2E_BASE_URL=https://cloud.example E2E_USER=alice E2E_PASSWORD=… E2E_GROUP="Design" npm run test:e2e
```

`E2E_GROUP` is optional — the display name of a Nextcloud group the account is
in, for the list test; without it that test is skipped.

## 🛠️ Contributing

- Contributions welcome — open a pull request and run the build and tests locally
Expand Down
2 changes: 1 addition & 1 deletion appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ What it does:
- 📱 A layout for phones: the post takes the width of the screen, the face inside the card, the composer's toolbar wrapping

You can pin your own posts to the top of your profile, bookmark any post, and see which hashtags the instance is using most. This is a partial implementation of ActivityPub and of the Mastodon client API. Blocking, muting and reporting (with a moderation panel in the administration settings) are supported, as are locked accounts with approvable follow requests. Polls are fully supported: create your own, and view and vote on federated ones. Profiles carry an avatar, a banner image and up to four profile metadata fields. Posts that link somewhere get a link preview card. Image (JPEG, PNG, GIF, WebP, AVIF, and HEIC from an iPhone where the server can read it), video (MP4, WebM, QuickTime) and audio attachments are supported, up to ten per post, each with alt text and a focal point. It does not offer lists.]]></description>
<version>0.19.53</version>
<version>0.19.58</version>
<licence>agpl</licence>
<author mail="benedikt.schaechner@web.de" homepage="https://benedikt.xn--schchner-2za.de">Benedikt Schächner</author>
<namespace>Social</namespace>
Expand Down
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "nextcloud/social",
"description": "Social app",
"license": "AGPL-3.0-or-later",
"version": "0.19.53",
"version": "0.19.58",
"minimum-stability": "stable",
"authors": [
{
Expand Down
2 changes: 1 addition & 1 deletion docs/Architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Nextcloud Social is a federated social networking app built on the W3C ActivityP
**App ID:** `social`
**Namespace:** `OCA\Social`
**License:** AGPL-3.0-or-later
**App version:** 0.19.53
**App version:** 0.19.58
**Supported Nextcloud versions:** 35 – 36
**Supported PHP versions:** 8.3 – 8.5

Expand Down
4 changes: 4 additions & 0 deletions docs/Technical-Debt.md
Original file line number Diff line number Diff line change
Expand Up @@ -369,6 +369,10 @@ sixty when twenty survive the check.
the only way a third party's Linked Data signature survives.
- **The superseded Custom Local API is gone** (0.19.39): nineteen routes, the
nine `getStream*()` methods and the seven `_dep` queries, and their tests.
- **A browser-test layer** (0.19.58): `tests/e2e/` and `.github/workflows/e2e.yml`
— Playwright against a throwaway Nextcloud on every pull request. 1,500 unit
tests and an integration suite could not see a page that stays empty because
a bundle did not load; the smoke suite can.

| Item | Outcome |
|---|---|
Expand Down
4 changes: 2 additions & 2 deletions js/social-social.js

Large diffs are not rendered by default.

50 changes: 48 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 6 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "social",
"description": "Nextcloud becomes part of the federated social networks!",
"version": "0.19.53",
"version": "0.19.58",
"authors": [
{
"name": "Benedikt Schächner",
Expand All @@ -16,14 +16,15 @@
"watch": "NODE_ENV=development webpack --progress --watch --config webpack.common.js",
"build": "NODE_ENV=production webpack --progress --config webpack.common.js",
"serve": "NODE_ENV=development webpack serve --progress --config webpack.common.js",
"lint": "eslint src tests/js js/social-adminSettings.js *.js *.mjs",
"lint": "eslint src tests/js tests/e2e js/social-adminSettings.js *.js *.mjs",
"typecheck": "tsc -p jsconfig.json",
"lint:fix": "eslint src tests/js js/social-adminSettings.js *.js *.mjs --fix",
"lint:fix": "eslint src tests/js tests/e2e js/social-adminSettings.js *.js *.mjs --fix",
"stylelint": "stylelint src css",
"stylelint:fix": "stylelint src css --fix",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"test:watch": "vitest"
"test:watch": "vitest",
"test:e2e": "playwright test -c tests/e2e/playwright.config.mjs"
},
"dependencies": {
"@nextcloud/auth": "^2.0.0",
Expand Down Expand Up @@ -69,6 +70,7 @@
"@nextcloud/eslint-config": "^9.0.1",
"@nextcloud/stylelint-config": "^3.2.2",
"@nextcloud/webpack-vue-config": "^6.0.0",
"@playwright/test": "^1.63.0",
"@vitejs/plugin-vue": "^6.0.8",
"@vitest/coverage-v8": "^5.0.0",
"@vue/compiler-sfc": "^3.5.42",
Expand Down
50 changes: 50 additions & 0 deletions tests/e2e/helpers.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

import { expect } from '@playwright/test'

export const USER = process.env.E2E_USER ?? 'admin'
export const PASSWORD = process.env.E2E_PASSWORD ?? 'admin'

/** Where the app lives, whether or not the instance has pretty URLs. */
export const APP = 'index.php/apps/social'

/**
* Signs in through Nextcloud's own login form, the way a person does.
*
* @param {import('@playwright/test').Page} page
* @param {string} user
* @param {string} password
*/
export async function login(page, user = USER, password = PASSWORD) {
await page.goto('index.php/login')
await page.locator('#user').fill(user)
await page.locator('#password').fill(password)
await page.locator('form[name="login"] button[type="submit"], #submit-form, button[type="submit"]').first().click()
// the dashboard, or wherever the instance sends a fresh session
await expect(page).not.toHaveURL(/\/login/)
}

/**
* Opens the app and waits for its sidebar, closing the first-run
* introduction if this account has never been here before.
*
* @param {import('@playwright/test').Page} page
* @param {string} path inside the app, e.g. '/discover'
*/
export async function openApp(page, path = '/') {
await page.goto(APP + path)
// the first paint of the app on a cold built-in server takes a while
await expect(page.locator('.app-navigation').first()).toBeVisible({ timeout: 45_000 })
const skip = page.locator('.first-run__skip')
if (await skip.isVisible().catch(() => false)) {
await skip.click()
}
}

/** The sidebar entry with this name. */
export function navEntry(page, name) {
return page.locator('.app-navigation-entry').filter({ hasText: name }).first()
}
Loading
Loading