Skip to content

Commit a2248a1

Browse files
authored
Merge pull request #422 from BentoBoxWorld/spike/plugwright-e2e
Spike: plugwright in-game E2E tests (validates #329 on 1.8.0)
2 parents 84db795 + 704531e commit a2248a1

12 files changed

Lines changed: 612 additions & 0 deletions

File tree

.github/workflows/e2e.yml

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
name: E2E (plugwright)
2+
3+
# In-game end-to-end tests via plugwright (boots a real Paper server + Mineflayer bot) —
4+
# the layer the Maven/MockBukkit unit tests can't reach. Run ON DEMAND as a regression check.
5+
#
6+
# Trigger it from the GitHub UI (Actions tab -> "E2E (plugwright)" -> "Run workflow", pick the
7+
# branch/tag) or from the CLI: gh workflow run e2e.yml --ref <branch-or-tag>
8+
#
9+
# It is deliberately NOT wired to every push/PR: a run boots a server and generates worlds
10+
# (~1-2 min) and is heavier/flakier than the unit tests. For a nightly regression, add:
11+
# schedule:
12+
# - cron: "0 3 * * *"
13+
on:
14+
workflow_dispatch:
15+
inputs:
16+
test_names:
17+
description: "Only run tests whose name contains this (comma-separated substrings). Blank = all."
18+
required: false
19+
default: ""
20+
21+
jobs:
22+
e2e:
23+
name: E2E tests
24+
runs-on: ubuntu-latest
25+
timeout-minutes: 25
26+
steps:
27+
- uses: actions/checkout@v4
28+
29+
- name: Set up JDK 21
30+
uses: actions/setup-java@v4
31+
with:
32+
distribution: temurin
33+
java-version: 21
34+
35+
- name: Set up Node.js
36+
uses: actions/setup-node@v4
37+
with:
38+
node-version: 20
39+
40+
- name: Set up Gradle (with caching)
41+
uses: gradle/actions/setup-gradle@748248ddd2a24f49513d8f472f81c3a07d4d50e1 # v4.4.4
42+
43+
# Reuse the Paper jar + downloaded libraries and the BentoBox/BSkyBlock jars across runs.
44+
# plugwright's clean step preserves server.jar / cache / libraries, and build.gradle.kts
45+
# caches the dependency jars in .deps, so this avoids re-downloading ~60 MB every run.
46+
- name: Cache Paper + dependency jars
47+
uses: actions/cache@v4
48+
with:
49+
path: |
50+
e2e/.deps
51+
e2e/run/server.jar
52+
e2e/run/cache
53+
e2e/run/libraries
54+
key: plugwright-${{ hashFiles('e2e/build.gradle.kts') }}
55+
56+
- name: Cache npm modules
57+
uses: actions/cache@v4
58+
with:
59+
path: e2e/src/test/e2e/node_modules
60+
key: e2e-npm-${{ hashFiles('e2e/src/test/e2e/package.json') }}
61+
62+
- name: Build Challenges jar (Maven)
63+
run: mvn -B -DskipTests package
64+
65+
- name: Run E2E tests
66+
working-directory: e2e
67+
env:
68+
PLUGWRIGHT_DEBUG: "1"
69+
# Bind the user-controlled input to an env var and reference it as a shell variable
70+
# below — never interpolate ${{ inputs.* }} directly into a run block (script injection).
71+
TEST_NAMES: ${{ inputs.test_names }}
72+
run: |
73+
if [ -n "$TEST_NAMES" ]; then
74+
./gradlew plugwrightTest "-PtestNames=$TEST_NAMES" --no-daemon
75+
else
76+
./gradlew plugwrightTest --no-daemon
77+
fi
78+
79+
# Always upload the server log + any plugwright report so a regression is debuggable.
80+
- name: Upload server log & report
81+
if: always()
82+
uses: actions/upload-artifact@v4
83+
with:
84+
name: e2e-logs
85+
path: |
86+
e2e/run/logs/**
87+
e2e/run/plugwright-report/**
88+
if-no-files-found: ignore
89+
retention-days: 14

e2e/.gitignore

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Plugwright server run directory (Paper jar, worlds, downloaded plugins, logs)
2+
run/
3+
# Cached dependency jars (BentoBox, BSkyBlock) downloaded by build.gradle.kts
4+
.deps/
5+
# Gradle
6+
.gradle/
7+
build/
8+
# Node / TypeScript test artifacts
9+
src/test/e2e/node_modules/
10+
src/test/e2e/dist/
11+
src/test/e2e/package-lock.json

e2e/README.md

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
# End-to-end (in-game) tests — plugwright spike
2+
3+
This is a spike using [plugwright](https://github.com/Drownek/plugwright) to verify Challenges
4+
**in a real running server** — something the JUnit/MockBukkit unit tests can't do. Plugwright boots
5+
a real Paper server, loads the plugins, and drives a headless [Mineflayer](https://github.com/PrismarineJS/mineflayer)
6+
bot that runs commands and clicks GUIs, then asserts on what the bot sees.
7+
8+
It is a **separate Gradle sidecar** — it does not touch the Maven build. It deploys *prebuilt* jars:
9+
10+
- **BentoBox 3.14.0** and **BSkyBlock 1.20.0** are downloaded (cached in `.deps/`).
11+
- The **Challenges** jar is taken from `../target/` (build it first with `mvn -DskipTests package`).
12+
13+
> **Gotcha that cost most of the spike:** BentoBox *addons* (BSkyBlock, Challenges) must be staged
14+
> into `plugins/BentoBox/addons/`, **not** `plugins/`. In `plugins/` they load as bare Paper plugins
15+
> and never register as gamemodes, so no `bskyblock_world` is created and every `/island`,
16+
> `/bsbadmin`, `/challenges` command comes back "Unknown". See `build.gradle.kts`.
17+
18+
## Running locally
19+
20+
```bash
21+
# 1. Build the Challenges jar (Maven)
22+
mvn -DskipTests package
23+
24+
# 2. Run the E2E tests (needs Java 21, Node 18+)
25+
cd e2e
26+
JAVA_HOME=/path/to/jdk-21 ./gradlew plugwrightTest
27+
28+
# add PLUGWRIGHT_DEBUG=1 to see every message the bot receives
29+
```
30+
31+
First run downloads Paper 1.21.11 (~50 MB) and the plugin jars; later runs reuse them
32+
(`run/server.jar`, `.deps/`). A full run is ~30–40s (server boot + world gen dominate).
33+
34+
## What the tests cover (`src/test/e2e/challenges.spec.ts`)
35+
36+
- **`bot can interact with the server`** — smoke test: bot joins Paper 1.21.11, ops, runs a command,
37+
receives the reply. Proves the whole stack (BentoBox 3.14.0 + BSkyBlock + Challenges 1.8.0) loads
38+
and is drivable.
39+
- **`confirmation prompts tell the player how to answer (#329)`** — opens the Challenges admin GUI,
40+
clicks "Challenge Wipe", and asserts the bot receives the confirmation instruction line
41+
("Type 'confirm' ... or 'cancel' ...") added in #415. A real in-game validation of a 1.8.0 feature.
42+
43+
## Notes / next steps
44+
45+
- Tests requiring an **island** (block/biome/completion flows) still need island bootstrap — either
46+
a bot-driven `/island create` (blueprint GUI) or a pre-staged world via `writeFiles`.
47+
- In CI this runs via `.github/workflows/e2e.yml` (manual `workflow_dispatch` for now — advisory,
48+
not a required check).

e2e/build.gradle.kts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
import java.net.URI
2+
3+
plugins {
4+
// Plugwright: boots a real Paper server, deploys plugins, drives Mineflayer bots.
5+
id("io.github.drownek.plugwright") version "2.0.2"
6+
}
7+
8+
// --- Dependency jars -------------------------------------------------------------------------
9+
// BentoBox is a Paper plugin (goes in plugins/). BSkyBlock and Challenges are BentoBox *addons*
10+
// and MUST live in plugins/BentoBox/addons/ so BentoBox loads them as gamemode/addon (creating
11+
// the gamemode world and registering commands) — dropping them straight into plugins/ makes them
12+
// load as bare Paper plugins that never register with BentoBox.
13+
14+
val depsDir = layout.projectDirectory.dir(".deps").asFile.apply { mkdirs() }
15+
16+
fun cachedJar(name: String, url: String): File {
17+
val f = File(depsDir, name)
18+
if (!f.exists()) {
19+
logger.lifecycle("plugwright: downloading $name ...")
20+
URI(url).toURL().openStream().use { input -> f.outputStream().use { input.copyTo(it) } }
21+
}
22+
return f
23+
}
24+
25+
val bentoboxJar = cachedJar(
26+
"BentoBox-3.14.0.jar",
27+
"https://github.com/BentoBoxWorld/BentoBox/releases/download/3.14.0/BentoBox-3.14.0.jar"
28+
)
29+
val bskyblockJar = cachedJar(
30+
"BSkyBlock-1.20.0.jar",
31+
"https://github.com/BentoBoxWorld/BSkyBlock/releases/download/1.20.0/BSkyBlock-1.20.0.jar"
32+
)
33+
34+
// Maven-built Challenges jar (version/profile suffix varies: -SNAPSHOT-LOCAL locally, plain on CI).
35+
val challengesJar: File = (file("../target").listFiles()?.toList() ?: emptyList())
36+
.firstOrNull {
37+
it.name.startsWith("Challenges-") && it.name.endsWith(".jar") &&
38+
!it.name.contains("sources") && !it.name.contains("javadoc")
39+
}
40+
?: throw GradleException(
41+
"Challenges jar not found in ../target — run 'mvn -q -DskipTests package' first."
42+
)
43+
44+
plugwright {
45+
minecraftVersion.set("1.21.11")
46+
acceptEula.set(true)
47+
// We deploy prebuilt jars (this is a Maven project, not a Gradle plugin build).
48+
useExternalPluginsOnly.set(true)
49+
testsDir.set(file("src/test/e2e"))
50+
jvmArgs.set(listOf("-Xmx3G"))
51+
52+
writeFiles {
53+
file("plugins/BentoBox.jar", bentoboxJar)
54+
file("plugins/BentoBox/addons/BSkyBlock.jar", bskyblockJar)
55+
file("plugins/BentoBox/addons/Challenges.jar", challengesJar)
56+
}
57+
}
44.6 KB
Binary file not shown.
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
distributionBase=GRADLE_USER_HOME
2+
distributionPath=wrapper/dists
3+
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10-bin.zip
4+
networkTimeout=10000
5+
validateDistributionUrl=true
6+
zipStoreBase=GRADLE_USER_HOME
7+
zipStorePath=wrapper/dists

0 commit comments

Comments
 (0)