SuperAPI AI Directory

roblox-studio-emulation

Testing & quality

View repository

Updated 10-17 16:13

About this skill

Emulating conditions a solo playtest can't reach

Two whole classes of testing look impossible from the MCP tools alone, so it's tempting to punt them to the user. Both are self-serviceable. Check here before claiming you need the user to test something.

For the normal dev loop see roblox-studio-workflow; for the MCP toolbox see roblox-studio-mcp.


1. Device emulation (mobile / tablet / touch) — via Studio's menu bar

Studio exposes no windows to macOS Accessibility (count windows → 0), so you can't resize it. But it exposes its entire menu bar, which is enough to drive the Test menu.

# one-time: the user must grant Terminal (or your host app) Accessibility permission
#   System Settings → Privacy & Security → Accessibility
# symptom if missing: "osascript is not allowed assistive access. (-1719)"

osascript -e 'tell application "Roblox Studio" to activate' osascript -e 'tell application "System Events" to tell process "RobloxStudio"
to click menu item "Device Emulator" of menu 1 of menu bar item "Test" of menu bar 1'


Process name is **`RobloxStudio`** (not "Roblox Studio", not "RobloxStudioBeta").

**Verify it took**, don't assume — start Play and read the real values:

```lua
local cam = workspace.CurrentCamera
local UIS = game:GetService("UserInputService")
-- desktop: ~1920x1080, TouchEnabled=false
-- emulated: device-sized viewport, TouchEnabled=TRUE
return cam.ViewportSize, UIS.TouchEnabled

TouchEnabled = true is the signal that touch-only code paths (thumbstick clearance, touch layout branches, POS_SHOWN offsets) are now live.

The full Test menu (useful items):

Item Notes
Device Emulator toggles emulation
Controller Emulator / Player Emulator gamepad / player-state emulation
Start Test Session required before Add Clients
Add Clients disabled in Play Solo; enabled inside a Test Session
Toggle Client View, Pause Client, End Session multi-client session control

Enumerate + check enabled state before clicking:

osascript -e 'tell application "System Events" to tell process "RobloxStudio" \
  to get name of every menu item of menu 1 of menu bar item "Test" of menu bar 1'
osascript -e 'tell application "System Events" to tell process "RobloxStudio" \
  to get enabled of menu item "Add Clients" of menu 1 of menu bar item "Test" of menu bar 1'

Limits

  • Picking a specific device (iPhone vs iPad vs …) uses an in-window dropdown that is NOT AX-exposed. You get whatever device is currently selected. Ask the user to set it once if you need a particular width — narrow phones are where layout actually breaks.
  • Toggling the emulator ends the current Play session and can drop the MCP link. Re-point with list_roblox_studios → set_active_studio, then start_stop_play.
  • Toggle it back OFF when done, or every later screenshot is a touch device.

2. Multiplayer features WITHOUT a second client — fire the real remotes

This is usually faster and more precise than launching a multi-client session. The Server and Client VMs share the DataModel, so a FireClient from the Server VM reaches the real client modules and drives everything downstream — signal handlers, cached state, UI refreshes.

-- datamodel_type = "Server"
local Remotes = require(game:GetService("ReplicatedStorage"):WaitForChild("Shared").Remotes)
local plr = game.Players:GetPlayers()[1]

-- pretend the server told us we joined someone's party
Remotes.event("PartyUpdated"):FireClient(plr, {
    ownerUserId = 99999999, ownerName = "TestHost",
    members = { { userId = plr.UserId, name = plr.DisplayName } },
})

Then assert on observable instance state from the Client VM (never module internals — separate VM, require gives a phantom copy):

-- datamodel_type = "Client"
local on = 0
for _, p in game:GetService("CollectionService"):GetTagged("Plot") do
    for _, d in p:GetDescendants() do
        if d:IsA("ProximityPrompt") and d.Name == "PartyPrompt" and d.Enabled then on += 1 end
    end
end
return on

Always test the round trip — fire the "joined" state, assert, then fire the "left" state (FireClient(plr, nil)) and assert it reverts. A one-way test hides stuck state, which is the scarier bug (a prompt stuck hidden = the feature is dead for that player).

Faking the preconditions

Client-side attribute writes are enough to set up a scenario, because client code reads the same attributes and GetAttributeChangedSignal fires locally:

-- datamodel_type = "Client" — pretend another player owns this plot
workspace.Map.Plots.Plot1:SetAttribute("OwnerUserId", 99999999)
-- ... run the test ...
workspace.Map.Plots.Plot1:SetAttribute("OwnerUserId", nil)  -- ALWAYS clean up

When this does NOT work

Code that requires the faked user to be a real online Player can't be faked this way. Example: a poll loop that does Players:GetPlayerByUserId(owner) and tears down anything failing that check will immediately undo your setup. Symptom: the thing you created vanishes within a second.

Fall back to either:

  • Test ▸ Start Test Session ▸ Add Clients for a genuine second player, or
  • unit-test the pure logic with synthetic inputs in the Client VM — mirror the shipped guard/branch verbatim and table-test it:
local cases = {
    { "nil payload",     nil,                 true  },
    { "empty string",    "",                  true  },
    { "garbage base64",  "!!!not-base64!!!",  true  },
    { "short buffer",    Base64.encode(short),true  },
    { "valid buffer",    Base64.encode(full), false },
}
-- assert blocked == expected for each; report N/N

That's a real, reportable result — just be explicit that it verifies the logic, not the full render path.


Reporting honestly

Distinguish these three, every time:

  • Verified live — drove it and observed the effect.
  • Logic-verified — table-tested the branch with synthetic inputs; full path untested.
  • Untested — say so, and say exactly what it needs.

Never let "it compiles and looks right" be reported as verified.