A World of Warcraft UI simulator for addon testing and UI rendering. Supports headless test workflows plus frame-tree and screenshot output — no WoW client required.
Join the wowless Discord: https://discord.gg/rTwWcfJXuz — we have a #wow-ui-sim channel there.
| Main Screen | Spellbook |
|---|---|
![]() |
![]() |
| Character Panel | Reputations Panel |
|---|---|
![]() |
![]() |
| Blacksmithing | Guild Panel |
|---|---|
![]() |
![]() |
| Store | Achievements Panel |
|---|---|
![]() |
![]() |
| World Map | Housing Panel |
|---|---|
![]() |
![]() |
| Talents Panel | Damage Meter |
|---|---|
![]() |
![]() |
| Game Menu |
|---|
![]() |
Run your addon's test suite in CI:
- uses: osso/wow-ui-sim@12.0.5
with:
addon: MyAddonThe action ref matches the WoW interface version it ships against
(e.g. @12.0.5). Use @master to track unreleased changes.
Tests live in Interface/AddOns/MyAddon/tests/*.lua:
test("frame name matches", function()
local f = CreateFrame("Frame", "MyFrame")
assertEquals("MyFrame", f:GetName())
end)A minimal end-to-end example is at
Osso/test-wow-addon — a TOC,
one Lua file, a tests/ folder, and a workflow that calls this action.
CASC profiles need a local WoW installation. wow-sim and wow-cli resolve the install
root for the compiled client profile in this order:
- CLI override (library callers only;
wow-simpasses none) WOW_INSTALL_PATHWOW_DATA_PATH(theDatadirectory; its parent is the root)- The stored entry for the active product
- Automatic detection: the Battle.net Agent
product.dbon Windows, then common install locations
A candidate is accepted only when <root>/Data/data exists.
The resolved path is stored per CASC product in install-paths.json:
| Platform | Location |
|---|---|
| Windows | %APPDATA%\wow-ui-sim\install-paths.json |
| Linux | $XDG_CONFIG_HOME/wow-ui-sim/install-paths.json (~/.config/... when unset) |
| macOS | ~/Library/Application Support/wow-ui-sim/install-paths.json |
The directory is the platform config directory (dirs::config_dir()). Entries are keyed by
CASC product code, which the compiled client-* feature selects:
| Key | Client profile |
|---|---|
wow |
Retail |
wowxptr |
PTR |
wowf |
WoW Forever |
wow_classic |
Wrath of the Lich King, Mists of Pandaria |
wow_classic_era |
Era, Anniversary |
Each entry accepts install_path (the install root that contains Data) and data_path
(the Data directory). install_path wins when both are present; a data_path-only entry
uses its parent as the root. A complete example, also stored as
docs/examples/install-paths.example.json:
{
"install_paths": {
"wow": {
"install_path": "C:\\Program Files (x86)\\World of Warcraft"
},
"wowxptr": {
"install_path": "D:\\Games\\World of Warcraft PTR"
},
"wowf": {
"install_path": "D:\\Games\\World of Warcraft Forever"
},
"wow_classic": {
"install_path": "D:\\Games\\World of Warcraft Classic",
"data_path": "D:\\Games\\World of Warcraft Classic\\Data"
},
"wow_classic_era": {
"data_path": "D:\\Games\\World of Warcraft Era\\Data"
}
}
}The file is written when a valid path comes from WOW_INSTALL_PATH, WOW_DATA_PATH, or
automatic detection; that entry replaces the stored one for the active product, so later
launches need no variable. A valid stored entry is used as-is and not rewritten. An entry
whose <root>/Data/data no longer exists is ignored with a stderr warning and replaced by
the next valid source. A missing file means no stored entries; malformed content is
reported and ignored, and startup continues. WOW_SIM_CASC=0 neither reads nor writes the
file.
These are the variables a player normally sets:
| Variable | Effect |
|---|---|
WOW_INSTALL_PATH |
WoW install root. Takes precedence over, and is stored over, the settings file. |
WOW_DATA_PATH |
WoW Data directory; its parent is used as the root. Lower priority than WOW_INSTALL_PATH. |
WOW_PRODUCT |
CASC product code used for detection. The compiled client-* feature fixes the client profile, so this cannot turn one client build into another. |
WOW_SIM_NO_SOUND |
1 or true skips audio initialization. |
WOW_SIM_SOFTWARE_RENDER |
1 or true forces the software GPU adapter for headless rendering (screenshot). |
WOW_SIM_MAX_CORES |
Linux only: restrict the process to the first N CPUs (default 2). |
WOW_SIM_STORAGE_DIR |
SavedVariables directory used when --session-wtf-path is absent. |
WOW_SIM_ADDONS_PATH |
Extra third-party addon root during default discovery. |
WOW_SIM_ADDONS_PATH_EXCLUSIVE |
1 or true makes WOW_SIM_ADDONS_PATH the only addon root. |
WOW_SIM_CASC=0 disables CASC asset loading, for example when game assets are unavailable.
Development and debugging variables
| Variable | Effect |
|---|---|
WOW_SIM_NO_ADDONS |
1 or true skips third-party addon loading. |
WOW_SIM_NO_SAVED_VARS |
1 or true skips loading WTF SavedVariables. |
WOW_SIM_SAVE_FAILED_ADDONS |
1 or true saves the SavedVariables of addons that raised Lua errors or named missing files while loading, as the client does. Failed SavedVariables input stays protected. |
WOW_SIM_DEBUG_ELEMENTS |
1 draws element borders and anchor points; see debug tools. |
WOW_SIM_DEBUG_BORDERS |
1 draws element borders only. |
WOW_SIM_DEBUG_ANCHORS |
1 draws anchor points only. |
WOW_SIM_VERBOSE |
Enables verbose addon-loading logging. |
Normal builds and runs use the shared desktop/local host selection:
python3 scripts/build-host.py --run
python3 scripts/build-host.py --build-host local --run
python3 scripts/build-host.py --check
python3 scripts/build-host.py --test --test integration FILTER -- --nocapture
scripts/start-profile.sh liveUse local while gaming on desktop. Raw Cargo stays local. See native workflow, profile flags, and runtime proof limits.
Use rustfmt for Rust code. rust-toolchain.toml pins Rust 1.99.1 and its rustfmt
component; rustfmt.toml selects the Rust 2024 syntax and style editions.
Rustup selects the pinned toolchain automatically inside this checkout.
cargo fmt --all --checkTo format only a file you changed, use
rustfmt --config skip_children=true path/to/changed.rs; skip_children prevents
formatting its child modules. Avoid workspace-wide formatting for focused changes.
GitHub releases provide Retail and WoW Forever packages for Linux x86-64, Windows x86-64, and macOS Apple Silicon. Mists packages are available for Linux x86-64 and Windows x86-64 only. Windows packages include a ZIP and an installer; macOS packages use a DMG. Forever package names include forever, and its installer and macOS app use separate identities.
Forever packages include selected spell data. For a local Forever build, run wow-cli casc sync-forever-spells before starting the simulator. It uses the installed pinned Forever client when available, or downloads the pinned DB2 files from Blizzard's CDN. See the spell data contract.
Retail and PTR dungeon score colours require a generated cache. Run wow-cli casc sync-dungeon-score-rarity for the active profile before running local tests or using those APIs. Retail release packages and Docker images include the cache. See the rarity contract.
Published addon-test images and CI/release workflows remain separate from native development. The Dockerfile builds Retail by default; use docker build --build-arg WOW_UI_SIM_PROFILE=wowforever . for a Forever image with its spell data.
Docker builds use .cargo/config.ci.toml for the default x86-64 CPU target;
local builds keep target-cpu=native in .cargo/config.toml.
Before publication, .github/scripts/verify_docker_image.py IMAGE checks
--version and addon tests under QEMU's Nehalem CPU model as well as natively.
It requires qemu-user-static (Ubuntu package); use --qemu PATH to select
another static qemu-x86_64 executable. Emulation runs offline with a read-only
fixture and does not require privileged containers or host binfmt changes.
# Run addon tests
docker run --rm \
-v ./MyAddon:/app/Interface/AddOns/MyAddon \
ghcr.io/osso/wow-ui-sim run-tests MyAddon
# Run with all Blizzard addons loaded
docker run --rm \
-v ./MyAddon:/app/Interface/AddOns/MyAddon \
ghcr.io/osso/wow-ui-sim --no-saved-vars run-tests MyAddon
# Render a screenshot with Mesa software Vulkan (no GPU required)
mkdir -p screenshots
docker run --rm -v "$(pwd)/screenshots:/out" \
ghcr.io/osso/wow-ui-sim screenshot -o /out/screenshot.webpFor deterministic timing in addon tests, see the manual clock example.
The image does not include WoW game assets; screenshots may omit textures unless assets are available to the container.
Default discovery combines WOW_SIM_ADDONS_PATH, local and bundled addon
folders, and the selected WoW installation. Setting WOW_SIM_ADDONS_PATH
alone does not isolate a test run.
Use repeatable CLI roots to replace those candidates:
wow-sim --addons-path /tmp/test/AddOns --addons-path /tmp/dependencies/AddOns run-tests MyAddonFor scripts that already set WOW_SIM_ADDONS_PATH, use the environment-only
mode. No script change is needed:
WOW_SIM_ADDONS_PATH_EXCLUSIVE=1 WOW_SIM_ADDONS_PATH=/tmp/test/AddOns wow-sim run-tests MyAddon
# A script that sets WOW_SIM_ADDONS_PATH internally:
WOW_SIM_ADDONS_PATH_EXCLUSIVE=1 WOW_SIM_BIN=/path/to/wow-sim ./run-tests.shtrue also enables exclusive mode; 0/false leave default discovery active.
CLI roots take precedence over environment selection. Each CLI value is one
addon root, not an individual addon folder. Use absolute paths: the simulator
changes its working directory to the executable directory before loading.
Missing roots are omitted; an empty exclusive selection does not restore the
default candidates.
Blizzard UI still loads from its profile cache. run-tests finds the bundled
TestFramework explicitly, without loading other bundled addons. Exclusive mode
also restricts test-file lookup and C_AddOns.LoadAddOn; it does not search
ambient Interface/TestAddOns. It is not a filesystem or Lua sandbox.
The composite action retains its existing default discovery. For an exclusive Docker run, mount the addon and its dependencies under a dedicated root, then pass that root to the image:
docker run --rm -v ./test-addons:/test-addons \
ghcr.io/osso/wow-ui-sim --addons-path /test-addons run-tests MyAddonThe root must contain MyAddon/MyAddon.toc and all required addon dependencies.
TestFramework does not need to be copied into that root. See the
selection contract.
Integration tests build wow-sim only unless utility features are requested.
The other five binaries require --features tools: wow-cli,
panel-visual-metrics, bench_spellbook, bench_steady_state, and
bench_talents. The two existing GUI-gated benches also require gui.
Use cargo build --bin wow-cli --features tools or
cargo build --bins --features tools for utility builds.
The release aliases and scripts/build-host.py --bin wow-cli --features tools enable this
feature automatically. Docker and full-suite test selection do not need it.
Build before using bare wow-cli commands; do not pass --features tools to the executable itself. For direct runs, use cargo run --bin wow-cli --features tools -- <args>.
See the build contract.
Historical replay: add --features tools before -- to the recorded Cargo utility commands in patch audit milestone tests, Mists world-map startup, and Windows port builds; original evidence is unchanged.
wow-cli generate spells --augment-attributes adds or refreshes only
SpellInfo.attributes_9 in the committed formatted spell table. It reads
SpellMisc.csv from the normal WoW data directory, selects DifficultyID=0 like
the full generator, and records the input hash and build 12.1.5.70391 in the
output header. Existing spell IDs and all other bytes remain unchanged. This
mode does not need Spell.csv; full wow-cli generate spells still requires
all original CSV inputs and writes the same attribute field.
Test files go in tests/ under your addon directory. Available assertions:
| Function | Description |
|---|---|
assertEquals(expected, actual) |
Strict equality |
assertNotEquals(expected, actual) |
Not equal |
assertTrue(value) / assertFalse(value) |
Truthy/falsy |
assertNil(value) / assertNotNil(value) |
Nil checks |
assertError(fn) |
Function throws an error |
assertType(expected, value) |
type(value) == expected |
assertAlmostEquals(expected, actual, tolerance?) |
Float comparison (default 0.001) |
assertContains(haystack, needle) |
String substring or table value |
assertStartsWith(str, prefix) |
String prefix |
assertEndsWith(str, suffix) |
String suffix |
assertMatches(str, pattern) |
Lua pattern match |
assertCount(expected, table) |
Table element count |
assertTableEquals(expected, actual) |
Deep table equality |
assertTableContains(table, subset) |
Table contains subset (deep) |
Async tests for timers and callbacks:
async_test("timer fires callback", function(done)
C_Timer.After(0, function()
done(function()
assertTrue(true)
end)
end)
end)The simulator exposes an A_Admin Lua namespace for controlling state in tests (player identity, combat, party, buffs, zone, economy, etc.). See docs/admin-api/.
GPL-3.0-only












