This commit is contained in:
lx
2026-07-06 14:01:11 -04:00
commit c4f97d729d
16468 changed files with 935321 additions and 0 deletions
@@ -0,0 +1,215 @@
local VirtualInput = require(script.Parent.VirtualInput)
local GuiService = game:GetService("GuiService")
local XPath = require(script.Parent.XPath)
local Element = {}
Element.__index = Element
function Element.new(argument)
local self = {}
if type(argument) == "string" then
self.path = XPath.new(argument)
elseif type(argument) == "table" and argument.__type == "XPath" then
self.path = argument
elseif type(argument) == "userdata" then
self.rbxInstance = argument
else
error("invalid parameter for element")
end
setmetatable(self, Element)
local scrollNums = self:_scrollingFrames(self.rbxInstance)
self.isInScrollingFrame = scrollNums ~= 0
return self
end
function Element:getAttribute(name)
return self:getRbxInstance()[name]
end
function Element:getLocation()
local guiOffset, _ = GuiService:GetGuiInset()
return self:getRbxInstance().AbsolutePosition + guiOffset
end
function Element:getRect()
local topLeft = self:getLocation()
local bottomRight = self:getSize() + topLeft
return Rect.new(topLeft.x, topLeft.y, bottomRight.x, bottomRight.y)
end
function Element:getSize()
return self:getRbxInstance().AbsoluteSize
end
function Element:getCenter()
return self:getLocation()+self:getSize()/2
end
function Element:getText()
return self:getRbxInstance().Text
end
function Element:isDisplayed()
return self:getRbxInstance().Visiable
end
function Element:isSelected()
return self:getRbxInstance().Selected
end
function Element:getRbxInstance()
if self.rbxInstance == nil and self.path ~= nil then
self.rbxInstance = self.path:waitForFirstInstance()
end
return self.rbxInstance
end
function Element:_override(class)
for k, v in pairs(class) do
if not k:find("^_") then
self[k] = v
end
end
end
function Element:centralizeInstance()
self:_centralizeInScrollingFrame(self:getRbxInstance())
end
function Element:centralize()
local instance = self:getRbxInstance()
if instance then
self:centralizeInstance()
else
self:centralizeWithInfiniteScrolling()
end
end
function Element:_scrollingFrames(instance)
if instance == nil or instance == game then return 0 end
local num = self:_scrollingFrames(instance.Parent)
if instance.ClassName == "ScrollingFrame" then num = num + 1 end
return num
end
function Element:_centralizeInScrollingFrame(child, parent)
if child == game then return end
parent = parent or child.Parent
if parent == game then return end
if parent.ClassName == "ScrollingFrame" then
self:_centralizeInScrollingFrame(parent, parent.Parent)
-- this is computational error tolerate.
local threshold = 2
--first scroll down to make child appears neas screen,
--so that we can access child.AbsolutPosition property
local isChildInScreen = false
while not isChildInScreen do
local prevChildPosition = child.AbsolutePosition
local prevCanvasPosition = parent.CanvasPosition
-- when scroll too much at one time, the element may move out side of screen immediately
-- its AbsoluteSize will not update. limit to 300
local scrollDistance = Vector2.new(math.min(300, parent.AbsoluteSize.X), math.min(300, parent.AbsoluteSize.Y))
parent.CanvasPosition = parent.CanvasPosition + scrollDistance
wait()
local deltaCanvas = (parent.CanvasPosition - prevCanvasPosition)
local isBottom = deltaCanvas.Magnitude <= threshold
local deltaChild = child.AbsolutePosition - prevChildPosition
isChildInScreen = isBottom or deltaChild.Magnitude > threshold
end
--second scroll to centerize the child, at most twice.
for _ = 1, 2 do
local frameCenter = parent.AbsolutePosition + parent.AbsoluteSize/2
local childCenter = child.AbsolutePosition + child.AbsoluteSize/2
local delta = childCenter - frameCenter
if delta.Magnitude <= threshold then break end
parent.CanvasPosition = parent.CanvasPosition + delta
wait()
end
else
self:_centralizeInScrollingFrame(child, parent.Parent)
end
end
function Element:_scrollToFindInstance(scrollingFrame, absPath)
-- first reset scrollingFrame to zero position
scrollingFrame.CanvasPosition = Vector2.new(0, 0)
local width = scrollingFrame.AbsoluteSize.X
local height = scrollingFrame.AbsoluteSize.Y
local isBottom = false
local instance
local threshold = 2
while not isBottom do
wait(0.1)
--if find the element then return
instance = absPath:getFirstInstance()
if instance then return instance end
--scroll
local oldPosition = scrollingFrame.CanvasPosition
scrollingFrame.CanvasPosition = scrollingFrame.CanvasPosition + Vector2.new(math.min(width, 300), math.min(height, 300))
--wait for content to refresh
local delta = scrollingFrame.CanvasPosition - oldPosition
isBottom = delta.Magnitude < threshold
--if it is the bottom, then return not found
end
return nil
end
function Element:centralizeWithInfiniteScrolling()
local instances, lastSeenIndex = self.path:getInstances()
if #instances > 0 then self:centralizeInstance() end
local lastSeenPath = self.path:copy()
while #lastSeenPath.data > lastSeenIndex do
table.remove(lastSeenPath.data)
end
local lastSeenInstance = lastSeenPath:getFirstInstance()
local lastScrollingFrame = nil
while true do
if lastSeenInstance.ClassName == "ScrollingFrame" then
lastScrollingFrame = lastSeenInstance
break
end
lastSeenInstance = lastSeenInstance.Parent
if lastSeenInstance == game then break end
end
if lastScrollingFrame == nil then return end
if self:_scrollToFindInstance(lastScrollingFrame, self.path) == nil then return end
self:_centralizeInScrollingFrame(self:getRbxInstance())
end
function Element:setPluginWindow()
local window = self.rbxInstance:FindFirstAncestorOfClass("DockWidgetPluginGui")
VirtualInput.setCurrentWindow(window)
end
function Element:click()
self:centralize()
self:setPluginWindow()
VirtualInput.click(self:getCenter())
end
function Element:sendKey(key)
self:setPluginWindow()
VirtualInput.hitKey(key)
end
function Element:sendText(str)
self:click()
wait(0)
VirtualInput.sendText(str)
end
function Element:tap()
self:centralize()
VirtualInput.tap(self:getCenter())
end
return Element
@@ -0,0 +1,79 @@
local InputVisualizer = {}
InputVisualizer.__index = InputVisualizer
local TweenService = game:GetService("TweenService")
local Debris = game:GetService("Debris")
local UserInputService = game:GetService("UserInputService")
function InputVisualizer.new()
local self = {}
local state, guiRoot = pcall(function() return game.CoreGui.Parent.CoreGui end)
if state == false then
local LocalPlayer = game.Players.LocalPlayer
while LocalPlayer == nil do
LocalPlayer = game.Players.LocalPlayer
wait()
end
guiRoot = LocalPlayer.PlayerGui
end
local GuiName = "InputVisualizer"
if guiRoot:FindFirstChild(GuiName) == nil then
local screenGui = Instance.new("ScreenGui")
screenGui.Name = GuiName
screenGui.DisplayOrder = 1000000
screenGui.Parent = guiRoot
end
guiRoot = guiRoot[GuiName]
self.guiRoot = guiRoot
setmetatable(self, InputVisualizer)
return self
end
function InputVisualizer:onInputBegan(input)
if input.UserInputType == Enum.UserInputType.MouseButton1 then
self:click(Vector2.new(input.Position.X, input.Position.Y))
elseif input.UserInputType == Enum.UserInputType.Touch then
self:click(Vector2.new(input.Position.X, input.Position.Y))
end
end
function InputVisualizer:click(vec2, pluginGui)
local delay = 0.5
local image = nil
if image == nil then
image = Instance.new("ImageLabel")
image.Image = "rbxassetid://1549893588"
image.BackgroundTransparency = 1
image.Parent = pluginGui or self.guiRoot
image.Size = UDim2.new(0, 20, 0, 20)
image.Name = "MouseClick"
image.ZIndex = 10
end
image.Visible = true
image.Position = UDim2.new(0, vec2.X-image.Size.X.Offset/2, 0, vec2.Y-image.Size.Y.Offset/2)
image.ImageTransparency = 0
local goal = {ImageTransparency = 1}
local tweenInfo = TweenInfo.new(0.5, Enum.EasingStyle.Quad, Enum.EasingDirection.InOut, 0, false)
local tween = TweenService:Create(image, tweenInfo, goal)
tween:Play()
Debris:AddItem(image, delay)
end
function InputVisualizer:enable()
self.handler = UserInputService.InputBegan:connect(
function(input, gameProcessed)
self:onInputBegan(input, gameProcessed)
end)
end
function InputVisualizer:disable()
if self.handler then
self.handler:Disconnect()
end
self.handler = nil
end
return InputVisualizer
@@ -0,0 +1,21 @@
local VIM = game:GetService("VirtualInputManager")
local RobloxEventSimulator = {}
RobloxEventSimulator.__index = RobloxEventSimulator
function RobloxEventSimulator.sendEvent(namespace, detail, detailType)
VIM:sendRobloxEvent(namespace, detail, detailType)
end
function RobloxEventSimulator.gotoPage(page)
RobloxEventSimulator.sendEvent("Navigations", string.format("{\"appName\":\"%s\"}", page), "Destination")
end
RobloxEventSimulator.Enums = {
pageHome = "Home",
pageGames = "Games",
pageAvatarEditor = "AvatarEditor",
pageChat = "Chat",
pageMore = "More",
}
return RobloxEventSimulator
@@ -0,0 +1,290 @@
local VirtualInputManager = game:GetService("VirtualInputManager")
local InputVisualizer = require(script.Parent.InputVisualizer):new()
local RunService = game:GetService("RunService")
local GuiService = game:GetService("GuiService")
local guiOffset, _ = GuiService:GetGuiInset()
local defaultTouchId = 123456
local runSet = {}
local signals = {}
RunService.Heartbeat:Connect(function(dt)
local finishedList = {}
for runable, _ in pairs(runSet) do
local finished = runable(dt)
if finished then
table.insert(finishedList, runable)
end
end
for _, toDelete in ipairs(finishedList) do
runSet[toDelete] = nil
end
for signal, _ in pairs(signals) do
if signal == false then
signals[signal] = nil
else
signal:Fire(dt)
end
end
end)
local function asyncRun(runable)
runSet[runable] = true
end
local function syncRun(runable)
local signal = Instance.new("BindableEvent")
signals[signal] = true
local dt = 0
while true do
local finished = runable(dt)
if finished then break end
dt = signal.Event:Wait()
end
signals[signal] = false
end
local VirtualInput = {}
local currentWindow = nil
function VirtualInput.setCurrentWindow(window)
local old = currentWindow
currentWindow = window
return old
end
function VirtualInput.sendMouseButtonEvent(x, y, button, isDown)
VirtualInputManager:SendMouseButtonEvent(x, y, button, isDown, currentWindow)
end
function VirtualInput.SendKeyEvent(isPressed, keyCode, isRepeated)
VirtualInputManager:SendKeyEvent(isPressed, keyCode, isRepeated, currentWindow)
end
function VirtualInput.SendMouseMoveEvent(x, y)
VirtualInputManager:SendMouseMoveEvent(x, y, currentWindow)
end
function VirtualInput.sendTextInputCharacterEvent(str)
VirtualInputManager:sendTextInputCharacterEvent(str, currentWindow)
end
function VirtualInput.SendMouseWheelEvent(x, y, isForwardScroll)
VirtualInputManager:SendMouseWheelEvent(x, y, isForwardScroll, currentWindow)
end
function VirtualInput.SendTouchEvent(touchId, state, x, y)
VirtualInputManager:SendTouchEvent(touchId, state, x, y)
end
function VirtualInput.mouseWheel(vec2, num)
local forward = false
if num < 0 then
forward = true
num = -num
end
for _ = 1, num do
VirtualInput.SendMouseWheelEvent(vec2.x, vec2.y, forward)
end
end
function VirtualInput.touchStart(vec2)
VirtualInput.SendTouchEvent(defaultTouchId, 0, vec2.x, vec2.y)
end
function VirtualInput.touchMove(vec2)
VirtualInput.SendTouchEvent(defaultTouchId, 1, vec2.x, vec2.y)
end
function VirtualInput.touchStop(vec2)
VirtualInput.SendTouchEvent(defaultTouchId, 2, vec2.x, vec2.y)
end
local function smoothSwipe(posFrom, posTo, duration)
local passed = 0
local started = false
return function(dt)
if not started then
VirtualInput.touchStart(posFrom)
started = true
else
passed = passed + dt
if duration and passed < duration then
local percent = passed / duration
local pos = (posTo - posFrom) * percent + posFrom
VirtualInput.touchMove(pos)
else
VirtualInput.touchMove(posTo)
VirtualInput.touchStop(posTo)
return true
end
end
return false
end
end
function VirtualInput.swipe(posFrom, posTo, duration, async)
if async == true then
asyncRun(smoothSwipe(posFrom, posTo, duration))
else
syncRun(smoothSwipe(posFrom, posTo, duration))
end
end
function VirtualInput.tap(vec2)
VirtualInput.touchStart(vec2)
VirtualInput.touchStop(vec2)
end
function VirtualInput.click(vec2)
InputVisualizer:click(vec2 - guiOffset, currentWindow)
VirtualInput.sendMouseButtonEvent(vec2.x, vec2.y, 0, true)
VirtualInput.sendMouseButtonEvent(vec2.x, vec2.y, 0, false)
end
function VirtualInput.rightClick(vec2)
VirtualInput.sendMouseButtonEvent(vec2.x, vec2.y, 1, true)
VirtualInput.sendMouseButtonEvent(vec2.x, vec2.y, 1, false)
end
function VirtualInput.mouseLeftDown(vec2)
VirtualInput.sendMouseButtonEvent(vec2.x, vec2.y, 0, true)
end
function VirtualInput.mouseLeftUp(vec2)
VirtualInput.sendMouseButtonEvent(vec2.x, vec2.y, 0, false)
end
function VirtualInput.mouseRightDown(vec2)
VirtualInput.sendMouseButtonEvent(vec2.x, vec2.y, 1, true)
end
function VirtualInput.mouseRightUp(vec2)
VirtualInput.sendMouseButtonEvent(vec2.x, vec2.y, 1, false)
end
function VirtualInput.pressKey(keyCode)
VirtualInput.SendKeyEvent(true, keyCode, false)
end
function VirtualInput.releaseKey(keyCode)
VirtualInput.SendKeyEvent(false, keyCode, false)
end
function VirtualInput.hitKey(keyCode)
VirtualInput.pressKey(keyCode)
VirtualInput.releaseKey(keyCode)
end
function VirtualInput.mouseMove(vec2)
VirtualInput.SendMouseMoveEvent(vec2.X, vec2.Y)
end
function VirtualInput.sendText(str)
VirtualInput.sendTextInputCharacterEvent(str)
end
local GamePad = {}
GamePad.__index = GamePad
local gamePadDeviceId = 123
GamePad.KeyCode = {
ButtonX = Enum.KeyCode.ButtonX,
ButtonY = Enum.KeyCode.ButtonY,
ButtonA = Enum.KeyCode.ButtonA,
ButtonB = Enum.KeyCode.ButtonB,
ButtonR1 = Enum.KeyCode.ButtonR1,
ButtonL1 = Enum.KeyCode.ButtonL1,
ButtonR2 = Enum.KeyCode.ButtonR2,
ButtonL2 = Enum.KeyCode.ButtonL2,
ButtonR3 = Enum.KeyCode.ButtonR3,
ButtonL3 = Enum.KeyCode.ButtonL3,
ButtonStart = Enum.KeyCode.ButtonStart,
ButtonSelect = Enum.KeyCode.ButtonSelect,
DPadLeft = Enum.KeyCode.DPadLeft,
DPadRight = Enum.KeyCode.DPadRight,
DPadUp = Enum.KeyCode.DPadUp,
DPadDown = Enum.KeyCode.DPadDown,
Thumbstick1 = Enum.KeyCode.Thumbstick1,
Thumbstick2 = Enum.KeyCode.Thumbstick2,
}
function GamePad.new()
local self = {deviceId = gamePadDeviceId}
gamePadDeviceId = gamePadDeviceId + 1
setmetatable(self, GamePad)
VirtualInputManager:HandleGamepadConnect(self.deviceId)
return self
end
function GamePad:disconnect()
VirtualInputManager:HandleGamepadDisconnect(self.deviceId)
end
function GamePad:pressButton(button)
VirtualInputManager:HandleGamepadButtonInput(self.deviceId, button, 1);
end
function GamePad:releaseButton(button)
VirtualInputManager:HandleGamepadButtonInput(self.deviceId, button, 0);
end
function GamePad:hitButton(button)
self:pressButton(button)
self:releaseButton(button)
end
function GamePad:moveStickTo(stick, vec2)
VirtualInputManager:HandleGamepadAxisInput(self.deviceId, stick, vec2.x, vec2.y, 0)
end
function GamePad:smoothMoveStickTo(stick, from, to, duration)
duration = duration or 0
if duration == 0 then
self:moveStickTo(stick, to)
return
end
local passed = 0
local function run(dt)
local ratio = passed / duration
passed = passed + dt
if ratio < 1 then
local pos = from + (to - from) * ratio
self:moveStickTo(stick, pos)
return false
else
self:moveStickTo(stick, to)
return true
end
end
syncRun(run)
end
function GamePad:swingStick(stick, pos, duration)
duration = duration or 0
local origin = Vector2.new(0, 0)
self:moveStickTo(stick, origin)
self:smoothMoveStickTo(stick, origin, pos, duration / 2)
self:smoothMoveStickTo(stick, pos, origin, duration / 2)
end
function GamePad:swingLeft(stick, duration)
self:swingStick(stick, Vector2.new(-1, 0), duration)
end
function GamePad:swingRight(stick, duration)
self:swingStick(stick, Vector2.new(1, 0), duration)
end
function GamePad:swingTop(stick, duration)
self:swingStick(stick, Vector2.new(0, 1), duration)
end
function GamePad:swingDown(stick, duration)
self:swingStick(stick, Vector2.new(0, -1), duration)
end
VirtualInput.GamePad = GamePad
return VirtualInput
@@ -0,0 +1,610 @@
local XPath = {}
XPath.__index = XPath
XPath.__type = "XPath"
local specialChars = [[\.=[],]]
local specialCharMap = {}
for i = 1, #specialChars do
local ch = specialChars:sub(i, i)
specialCharMap[ch] = true
end
function XPath.addSlash(str)
local tab = {}
for i = 1, str:len() do
local ch = str:sub(i, i)
if specialCharMap[ch] then
table.insert(tab, "\\")
end
table.insert(tab, ch)
end
return table.concat(tab)
end
function XPath.removeSlash(str)
local tab = {}
local isBackSlash = false
for i = 1, str:len() do
local ch = str:sub(i, i)
if ch == "\\" and isBackSlash == false then
isBackSlash = true
else
if isBackSlash == true then
isBackSlash = false
end
table.insert(tab, ch)
end
end
return table.concat(tab)
end
local function splitByCharWithSlash(s, token)
local result = {}
if s == nil or s == "" then return result end
local isBackSlash = false
local lastIndex = 1
for i = 1, s:len() do
local ch = s:sub(i, i)
if ch == "\\" and isBackSlash == false then
isBackSlash = true
else
if isBackSlash == true then
isBackSlash = false
else
if ch == token then
table.insert(result, (s:sub(lastIndex, i-1)))
lastIndex = i+1
end
end
end
end
table.insert(result, (s:sub(lastIndex, s:len())))
return result
end
local function deepCopy(t)
local t2 = {}
for k, v in pairs(t) do
if type(v) == "table" then
t2[k] = deepCopy(v)
else
t2[k] = v
end
end
return t2
end
function XPath.new(obj, root)
local self = {data = {}, root = root, waitDelay = 0.2, waitTimeOut = 2}
setmetatable(self, XPath)
if type(obj) == "string" then
self:fromString(obj)
elseif type(obj) == "userdata" then
local current = obj
while current do
table.insert(self.data, 1, {name = current.Name})
current = current.Parent
end
self.data[1] = {name = "game"}
elseif getmetatable(obj).__type == XPath.__type then
return obj:copy()
else
error("unknown parameter ", obj)
end
return self
end
function XPath:size()
return #self.data
end
function XPath:mergeFilter(index, additionalFilter)
if index > self:size() then error("bad index") end
local filter = self.data[index].filter or {}
local filterDict = {}
for _, item in ipairs(filter) do
filterDict[item.key] = item.value
end
if additionalFilter then
for _, item in ipairs(additionalFilter) do
filterDict[item.key] = tostring(item.value)
end
end
local newFilter = {}
for k, v in pairs(filterDict) do
table.insert(newFilter, {key = k, value = v})
end
self.data[index].filter = newFilter
return self
end
function XPath:fromString(str)
local inBracket = false
local isBackSlash = false
local data = {}
local lastIndex = 1
str = str .. "."
for i = 1, str:len() do
local ch = str:sub(i, i)
if ch == "\\" and isBackSlash == false then
isBackSlash = true
else
if isBackSlash == true then
isBackSlash = false
else
if ch == "." then
if not inBracket then
table.insert(data, {name = str:sub(lastIndex, i-1)})
lastIndex = i+1
end
elseif ch == "[" then
if inBracket == true then
error("no nested bracket allowed: " .. str)
end
inBracket = true
elseif ch == "]" then
if not inBracket then
error("unbalanced brackets: " .. str)
end
inBracket = false
end
end
end
end
if inBracket == true then error("unbalanced brackets: " .. str) end
for i = 1, #data do
local name, filters = data[i].name:match("%s*(.*[^\\])%[(.*[^\\])%]%s*")
if name == nil then
filters = ""
name = data[i].name
end
if name ~= nil then
data[i].name = XPath.removeSlash(name)
local filterArray = splitByCharWithSlash(filters, ",")
local filterObjs = {}
for _, filter in ipairs(filterArray) do
local key, value = filter:match("^%s*(.-[^\\])%s*=%s*(.-)%s*$")
if key then
table.insert(filterObjs, {key = key, value = value})
end
end
data[i].filter = filterObjs
end
end
self.data = data
end
function XPath:copy()
local result = deepCopy(self)
setmetatable(result, XPath)
return result
end
function XPath:parent()
local newOne = self:copy()
if #newOne.data <= 1 then
-- error("this is the root")
return newOne
else
table.remove(newOne.data, #newOne.data)
end
return newOne
end
function XPath:_itemToString(item)
local result = XPath.addSlash(item.name)
if item.filter and #item.filter > 0 then
local filter = {}
for _, v in ipairs(item.filter) do
table.insert(filter, v.key .. " = " ..v.value)
end
result = result .. "[" .. table.concat(filter, ", ") .. "]"
end
return result
end
function XPath:toString(arg)
if arg == nil then
local tab = {}
for _, item in ipairs(self.data) do
table.insert(tab, self:_itemToString(item))
end
return table.concat(tab, ".")
elseif type(arg) == "number" then
if arg < 0 then arg = self:size() + arg + 1 end
if arg > self:size() or arg < 1 then error("invalid index") end
return self:_itemToString(self.data[arg])
elseif type(arg) == "table" then
return self:_itemToString(arg)
end
end
function XPath:hasChild(child)
return child:relative(self) ~= nil
end
function XPath:relative(root)
if self:size()<#root.data then return nil end
local newRoot = root:copy()
local newSelf = self:copy()
while #newRoot.data > 0 do
if newRoot.data[1].name ~= newSelf.data[1].name then return nil end
table.remove(newRoot.data, 1)
table.remove(newSelf.data, 1)
end
return newSelf
end
function XPath:cat(path)
local newOne = self:copy()
for _, k in ipairs(path.data) do
table.insert(newOne.data, k)
end
return newOne
end
function XPath:clearFilter()
for i = 1, self:size() do
self.data[i].filter = nil
end
return self
end
local function getProperty(instance, property)
local state, result = pcall(function() return instance[property] end )
return state == true and result or nil
end
local function propertyEqual(lhs, rhs)
return tostring(lhs) == tostring(rhs)
end
local function propertyMatch(prop, expr)
prop = tostring(prop)
expr = tostring(expr)
return expr == "*" or prop == expr
end
local function findChildrenByName(instance, name)
local children = instance:GetChildren()
local result = {}
for _, child in ipairs(children) do
if propertyMatch(getProperty(child, "Name"), name) then
table.insert(result, child)
end
end
-- for game.Players, their is no child "LocalPlayer", but you can access it.
if #result == 0 then
local instance = getProperty(instance, name)
if instance then
table.insert(result, instance)
end
end
return result
end
local function findChildren(instances, name)
local result = {}
for _, instance in ipairs(instances) do
local children = findChildrenByName(instance, name)
for _, child in ipairs(children) do
table.insert(result, child)
end
end
return result
end
local function findCandidates(instances, path)
for _, name in ipairs(path) do
instances = findChildren(instances, name)
end
return instances
end
local function passFilter(instance, filter)
local key = filter.key
local path, propertyName = key:match("^%.?(.*[^\\])%.(%w-)$")
if propertyName == nil then
path = ""
propertyName = key:match("^%.?(%w-)$")
end
local pathData = splitByCharWithSlash(path, ".")
for i = 1, #pathData do pathData[i] = XPath.removeSlash(pathData[i]) end
local canditates = findCandidates({instance}, pathData)
for _, canditate in ipairs(canditates) do
local property = getProperty(canditate, propertyName)
if propertyMatch(property, XPath.removeSlash(filter.value)) then
return true
end
end
return false
end
local function passFilters(instance, filters)
for _, filter in ipairs(filters) do
if not passFilter(instance, filter) then
return false
end
end
return true
end
local function applyFilters(canditates, filters)
if filters == nil then return canditates end
local result = {}
for _, canditate in ipairs(canditates) do
if passFilters(canditate, filters) then
table.insert(result, canditate)
end
end
return result
end
function XPath:getFirstInstance()
local instances = self:getInstances()
if #instances == 0 then return nil else return instances[1] end
end
function XPath:getInstances()
if self:size() <1 then error("instance " .. self:toString() .. " does not exist") end
if self.root == nil and self.data[1].name ~= "game" then error("instance " .. self:toString() .. " does not exist") end
local instances = {self.root or game}
local i = self.root and 1 or 2
while i <= self:size() do
local name = self.data[i].name
local filters = self.data[i].filter
local candidates = findChildren(instances, name)
instances = applyFilters(candidates, filters)
if #instances == 0 then return instances, i-1 end
i = i + 1
end
return instances, i-1
end
function XPath:setWait(timeOut, delay)
self.waitDelay = delay
self.waitTimeOut = timeOut
return self
end
function XPath:waitFor(execute, condition, delay, timeOut)
delay = delay or self.waitDelay
timeOut = timeOut or self.waitTimeOut
timeOut = tick()+timeOut
while true do
local result = execute()
if condition(result) then
return result, true
end
wait(delay)
if tick() > timeOut then return result, false end
end
end
function XPath:waitForFirstInstance()
-- return self:waitForInstances(function(instances) return #instances >0 end)[1]
local instances = self:waitForNInstances(1)
if instances ~= nil and #instances > 0 then return instances[1] end
return nil
end
function XPath:waitForInstances(condition)
if type(condition) ~= "function" then error("arg #1 should be a function") end
return self:waitFor(function()
return self:getInstances()
end, condition)
end
function XPath:waitForDisappear()
local result, state = self:waitForInstances(function(instances) return #instances == 0 end)
return state == true
end
function XPath:waitForNInstances(n)
return self:waitForInstances(function(instances)
return #instances >= n
end)
end
local function makeInstance(className, props, children)
local instance = Instance.new(className)
if children then
for _, child in ipairs(children) do
child.Parent = instance
end
end
if props then
for k, v in pairs(props) do
instance[k] = v
end
end
return instance
end
local function test()
local function closeTo(lhs, rhs, err)
return math.abs(lhs-rhs) <= err
end
local specialChars = [[special chars !"#$%&'()*+,-./:;<=>?@[]\^_`{|}~]]
local convertedSpecialChars = [[special chars !"#$%&'()*+\,-\./:;<\=>?@\[\]\\^_`{|}~]]
local root = nil
if workspace:FindFirstChild("root") == nil then
root = makeInstance("Folder",
{
Name = "root",
Parent = workspace
}, {
makeInstance("Frame", {Name = "Frame"}, {
makeInstance("TextButton", {
Text = "Button1"
}),
makeInstance("TextLabel", {
Text = "Label1"
}),
makeInstance("ImageButton", {
})
}),
makeInstance("Frame", {Name = "Frame"}, {
makeInstance("TextButton", {
Text = "Button2"
}),
makeInstance("TextLabel", {
Text = "Label2"
})
}),
makeInstance("Frame", {Name = "Frame"}, {
makeInstance("TextLabel", {Text = "Label3"}),
makeInstance("Frame", {Name = specialChars}, {
makeInstance("TextButton", {Name = "TextButton3"})
}),
makeInstance("TextLabel", {Name = "SpecialCharLabel", Text = specialChars})
})
})
end
local containerPath = XPath.new("game.Workspace.root.Frame")
local getContainerDetail = function(root)
return {
textButton = XPath.new("TextButton", root),
textLabel = XPath.new("TextLabel", root),
}
end
local createSearch = function(container, relativePath, property, value)
local rootPath = container:copy()
local filter = {{key = "." .. relativePath:toString().."."..property, value = value}}
rootPath:mergeFilter(rootPath:size(), filter)
return rootPath
end
local rootPath = createSearch(containerPath, getContainerDetail().textButton, "Text", "Button2")
print("createSearch:", rootPath:toString())
local rootInstance = rootPath:waitForFirstInstance()
assert(rootInstance)
local containerDetail = getContainerDetail(rootInstance)
local label2 = containerDetail.textLabel:waitForFirstInstance()
assert(label2.Text == "Label2")
print("relative path test")
local path = XPath.new("game.Workspace.root.Frame[.TextButton.Text = Button2]")
local instance = path:getFirstInstance()
assert(instance)
local relativePath = XPath.new("TextButton", instance)
local instance = relativePath:getFirstInstance()
assert(instance.Text == "Button2")
local path = XPath.new("game.Workspace.root.Frame[.TextButton.Text = Button2, .ClassName = Frame].TextLabel")
local instance = path:getFirstInstance()
assert(instance.Text=="Label2")
local newPathString = XPath.new(instance):toString()
assert(newPathString=="game.Workspace.root.Frame.TextLabel")
local pathStr = "game.Workspace.root.Frame[."..convertedSpecialChars..".TextButton3.Name = TextButton3].TextLabel"
local path = XPath.new(pathStr)
assert(path:toString()==pathStr)
local instance = path:getFirstInstance()
assert(instance.Text=="Label3")
local pathStr = "game.Workspace.root.Frame[.SpecialCharLabel.Text = "..convertedSpecialChars.."].TextLabel"
local path = XPath.new(pathStr)
assert(path:toString()==pathStr)
local instance = path:getFirstInstance()
assert(instance.Text=="Label3")
local pathStr = "game.Workspace.root.Frame."..convertedSpecialChars..".TextButton3"
local path = XPath.new(pathStr)
local newPathStr = path:toString()
assert(newPathStr==pathStr)
local instance = path:getFirstInstance()
assert(instance.Name=="TextButton3")
local rootPath = XPath.new("game.Workspace.root")
local path = XPath.new("game.Workspace.root.Frame[.TextButton.Text = Button2, .ClassName = Frame].TextLabel")
local relativePath = path:relative(rootPath)
relativePath:clearFilter()
print("testing getFirstInstance()")
print(path:toString())
local instance = path:getFirstInstance()
assert(instance.Text == "Label2")
print("testing wildcard *")
path = XPath.new("game.Workspace.root.*[.TextButton.Text = Button2].TextLabel")
instance = path:getFirstInstance()
assert(instance.Text == "Label2")
path = XPath.new("game.Workspace.root.*[.ImageButton.Name = *].TextLabel")
instance = path:getFirstInstance()
assert(instance.Text == "Label1")
local timeBefore = nil
print("testing timeout waitForNInstances() ")
path = XPath.new("game.Workspace.root.Frame.TextLabel")
timeBefore = tick()
local instances, state = path:setWait(2):waitForNInstances(5)
assert(closeTo(tick() - timeBefore, 2, 0.5))
assert(state == false)
print("testing normal waitForNInstances() ")
timeBefore = tick()
spawn(function()
wait(2)
makeInstance("Frame", {
Name = "Frame",
Parent = workspace.root
}, {
makeInstance("TextButton", {
Text = "Button3"
}),
makeInstance("TextLabel", {
Text = "Label3"
})
})
end)
local instances = path:setWait(5):waitForNInstances(5)
assert(closeTo(tick() - timeBefore, 2, 0.5))
assert(#instances >= 5)
print("testing getFirstInstance() ")
path = XPath.new("game.Workspace.root.Frame[.TextButton.Text = Button3]")
instance = path:getFirstInstance()
assert(instance.TextButton.Text == "Button3")
print("testing timeout waitForDisappear() ")
timeBefore = tick()
local notExist = path:setWait(2):waitForDisappear()
assert(notExist == false)
assert(closeTo(tick() - timeBefore, 2, 0.5))
print("testing normal waitForDisappear() ")
timeBefore = tick()
spawn(function()
wait(2)
instance:Destroy()
end)
local notExist = path:setWait(5):waitForDisappear()
assert(closeTo(tick() - timeBefore, 2, 0.5))
assert(notExist == true)
print("test finised")
end
--test()
return XPath
@@ -0,0 +1,96 @@
return function()
local XPath = require(script.Parent.XPath)
local function makeInstance(className, props, children)
local instance = Instance.new(className)
if children then
for _, child in ipairs(children) do
child.Parent = instance
end
end
if props then
for k, v in pairs(props) do
instance[k] = v
end
end
return instance
end
local specialChars = [[special chars !"#$%&'()*+,-./:;<=>?@[]\^_`{|}~]]
local convertedSpecialChars = [[special chars !"#$%&'()*+\,-\./:;<\=>?@\[\]\\^_`{|}~]]
local root = makeInstance("Folder",
{
Name = "root",
Parent = workspace
}, {
makeInstance("Frame", {Name = "Frame"}, {
makeInstance("TextButton", {
Text = "Button1"
}),
makeInstance("TextLabel", {
Text = "Label1"
}),
makeInstance("ImageButton", {
})
}),
makeInstance("Frame", {Name = "Frame"}, {
makeInstance("TextButton", {
Text = "Button2"
}),
makeInstance("TextLabel", {
Text = "Label2"
})
}),
makeInstance("Frame", {Name = "Frame"}, {
makeInstance("TextLabel", {Text = "Label3"}),
makeInstance("Frame", {Name = specialChars}, {
makeInstance("TextButton", {Name = "TextButton3"})
}),
makeInstance("TextLabel", {Name = "SpecialCharLabel", Text = specialChars})
})
})
describe("basic tests", function()
it("getFirstInstance should work", function()
local path = XPath.new("game.Workspace.root.Frame[.TextButton.Text = Button2, .ClassName = Frame].TextLabel")
local instance = path:getFirstInstance()
expect(instance.Text).to.equal("Label2")
expect(XPath.new(instance):toString()).to.equal("game.Workspace.root.Frame.TextLabel")
end)
it("wildcard should work", function()
local path = XPath.new("game.Workspace.root.*[.TextButton.Text = Button2].TextLabel")
local instance = path:getFirstInstance()
expect(instance.Text).to.equal("Label2")
end)
it("wildcard on property should work", function()
local path = XPath.new("game.Workspace.root.*[.ImageButton.Name = *].TextLabel")
local instance = path:getFirstInstance()
expect(instance.Text).to.equal("Label1")
end)
end)
describe("should work with special chars", function()
it("should work with special chars in path", function()
local pathStr = "game.Workspace.root.Frame."..convertedSpecialChars..".TextButton3"
local path = XPath.new(pathStr)
expect(path:toString()).to.equal(pathStr)
local instance = path:getFirstInstance()
expect(instance.Name).to.equal("TextButton3")
end)
it("should work with special chars in filter keys", function()
local pathStr = "game.Workspace.root.Frame[."..convertedSpecialChars..".TextButton3.Name = TextButton3].TextLabel"
local path = XPath.new(pathStr)
expect(path:toString()).to.equal(pathStr)
local instance = path:getFirstInstance()
expect(instance.Text).to.equal("Label3")
end)
it("should work with special chars in filter values", function()
local pathStr = "game.Workspace.root.Frame[.SpecialCharLabel.Text = "..convertedSpecialChars.."].TextLabel"
local path = XPath.new(pathStr)
expect(path:toString()).to.equal(pathStr)
local instance = path:getFirstInstance()
expect(instance.Text).to.equal("Label3")
end)
end)
end
@@ -0,0 +1,223 @@
--to make ScriptAnalyzer Happy
describe = nil
step = nil
expect = nil
include = nil
-------------------------------
local XPath = require(game.CoreGui.RobloxGui.Modules.Rhodium.XPath)
local bottomBar = XPath.new("game.CoreGui.BottomBar")
local topBar = XPath.new("game.CoreGui.App.AppRouter.Home.Contents.TopBar.TopBar")
local content = XPath.new("game.CoreGui.App.AppRouter.Home.Contents.Scroller.scrollingFrame.Content")
local MobileAppElements = {
bottomBar = bottomBar,
topBar = topBar,
pageName = topBar:cat(XPath.new("NavBar.Title")),
robuxButton = topBar:cat(XPath.new("NavBar.RightIcons.Robux")),
notificationsButton = topBar:cat(XPath.new("NavBar.RightIcons.Notifications")),
searchInputBox = topBar:cat(XPath.new("NavBar.RightIcons.Search.SearchBar.SearchTouchArea.SearchFrameImage.SearchBox")),
verticalScrollingFrame = XPath.new("game.CoreGui.App.AppRouter.Home.Contents.Scroller.scrollingFrame"),
homeScreen = XPath.new("game.CoreGui.App.AppRouter.Home"),
backButton = topBar:cat(XPath.new("Layout.BackButton")),
avatarButton = bottomBar:cat(XPath.new("Contents.Frame.AvatarButton.ButtonFrame.Title")),
catalogButton = bottomBar:cat(XPath.new("Contents.Frame.CatalogButton.ButtonFrame.Title")),
chatButton = bottomBar:cat(XPath.new("Contents.Frame.ChatButton.ButtonFrame.Title")),
friendsButton = bottomBar:cat(XPath.new("Contents.Frame.FriendsButton.ButtonFrame.Title")),
gamesButton = bottomBar:cat(XPath.new("Contents.Frame.GamesButton.ButtonFrame.Title")),
homeButton = bottomBar:cat(XPath.new("Contents.Frame.HomeButton.ButtonFrame.Title")),
moreButton = bottomBar:cat(XPath.new("Contents.Frame.MoreButton.ButtonFrame.Title")),
userNameText = content:cat(XPath.new("TitleSection.BuildersClubUsernameFrame.Username")),
friendCarousel = content:cat(XPath.new("FriendSection.CarouselFrame.MainFrame.Carousel")),
friendSeeAllText = content:cat(XPath.new("FriendSection.Container.Header.Spacer.Button.Text")),
friendTitle = content:cat(XPath.new("FriendSection.Container.Header.Title")),
gameCategoryEntry = content:cat(XPath.new("GameDisplay.*[.ClassName = Frame]")),
viewFeedText = content:cat(XPath.new("FeedSection.MyFeedButton.Button.Text")),
listPicker = XPath.new("game.CoreGui.PortalUI.Contents.AnimatedPopout.Scroller"),
listPickerItem = XPath.new("game.CoreGui.PortalUI.Contents.AnimatedPopout.Scroller.*"),
currentGameList = XPath.new("game.CoreGui.App.AppRouter.*[.ClassName = ScreenGui, .Enabled = true]"),
----avatar editor
avatarEditorScene = XPath.new("game.Workspace.AvatarEditorScene"),
character = XPath.new("game.Workspace.Folder.Character"),
r6r15SwitchFrame = XPath.new("game.CoreGui.ScreenGui.RootGui.AvatarTypeSwitch"),
r6r15Switch = XPath.new("game.CoreGui.ScreenGui.RootGui.AvatarTypeSwitch.Switch"),
selectTabButton = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TopMenuContainer.SelectedIcon"),
closeTabButton = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TopMenuContainer.CloseButton"),
assetButton = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.ScrollingFrame.*[.ClassName = ImageButton]"),
fullViewButton = XPath.new("game.CoreGui.ScreenGui.RootGui.TopFrame.FullViewButton"),
rightFrame = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame"),
rightFrameText = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.ScrollingFrame.TextLabel"),
groupTabs = {
recentButton = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TopMenuContainer.CategoryScroller.CategoryButtonRecent"),
clothingButton = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TopMenuContainer.CategoryScroller.CategoryButtonClothing"),
bodyButton = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TopMenuContainer.CategoryScroller.CategoryButtonBody"),
animationButton = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TopMenuContainer.CategoryScroller.CategoryButtonAnimation"),
outfitsButton = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TopMenuContainer.CategoryScroller.CategoryButtonOutfits"),
},
groupTabs_portrait = {
recentButton = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TopMenuContainer.CategoryButtonRecent"),
clothingButton = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TopMenuContainer.CategoryButtonClothing"),
bodyButton = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TopMenuContainer.CategoryButtonBody"),
animationButton = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TopMenuContainer.CategoryButtonAnimation"),
outfitsButton = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TopMenuContainer.CategoryButtonOutfits"),
},
recentTabs = {
tabAll = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabListContainer.TabList.Contents.TabRecent All"),
tabClothing = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabListContainer.TabList.Contents.TabRecent Clothing"),
tabBody = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabListContainer.TabList.Contents.TabRecent Body"),
tabAnimation = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabListContainer.TabList.Contents.TabRecent Animation"),
tabOutfits = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabListContainer.TabList.Contents.TabRecent Outfits"),
},
recentTabs_portrait = {
tabAll = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabList.Contents.TabRecent All"),
tabClothing = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabList.Contents.TabRecent Clothing"),
tabBody = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabList.Contents.TabRecent Body"),
tabAnimation = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabList.Contents.TabRecent Animation"),
},
clothingTabs = {
tabBack = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabListContainer.TabList.Contents.TabBack Accessories"),
tabFace = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabListContainer.TabList.Contents.TabFace Accessories"),
tabFront = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabListContainer.TabList.Contents.TabFront Accessories"),
tabGear = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabListContainer.TabList.Contents.TabGear"),
tabHair = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabListContainer.TabList.Contents.TabHair"),
tabHats = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabListContainer.TabList.Contents.TabHats"),
tabNeck = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabListContainer.TabList.Contents.TabNeck Accessories"),
tabPants = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabListContainer.TabList.Contents.TabPants"),
tabShirts = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabListContainer.TabList.Contents.TabShirts"),
tabShoulder = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabListContainer.TabList.Contents.TabShoulder Accessories"),
tabWaist = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabListContainer.TabList.Contents.TabWaist Accessories"),
},
clothingTabs_portrait = {
tabBack = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabList.Contents.TabBack Accessories"),
tabFace = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabList.Contents.TabFace Accessories"),
tabFront = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabList.Contents.TabFront Accessories"),
tabGear = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabList.Contents.TabGear"),
tabHair = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabList.Contents.TabHair"),
tabHats = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabList.Contents.TabHats"),
tabNeck = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabList.Contents.TabNeck Accessories"),
tabPants = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabList.Contents.TabPants"),
tabShirts = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabList.Contents.TabShirts"),
tabShoulder = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabList.Contents.TabShoulder Accessories"),
tabWaist = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabList.Contents.TabWaist Accessories"),
},
bodyTabs = {
tabFaces = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabListContainer.TabList.Contents.TabFaces"),
tabHeads = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabListContainer.TabList.Contents.TabHeads"),
tabLeftArms = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabListContainer.TabList.Contents.TabLeft Arms"),
tabLeftLegs = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabListContainer.TabList.Contents.TabLeft Legs"),
tabRightArms = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabListContainer.TabList.Contents.TabRight Arms"),
tabRightLegs = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabListContainer.TabList.Contents.TabRight Legs"),
tabScale = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabListContainer.TabList.Contents.TabScale"),
tabSkinTone = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabListContainer.TabList.Contents.TabSkin Tone"),
tabTorsos = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabListContainer.TabList.Contents.TabTorsos"),
},
bodyTabs_portrait = {
tabFaces = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabList.Contents.TabFaces"),
tabHeads = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabList.Contents.TabHeads"),
tabLeftArms = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabList.Contents.TabLeft Arms"),
tabLeftLegs = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabList.Contents.TabLeft Legs"),
tabRightArms = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabList.Contents.TabRight Arms"),
tabRightLegs = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabList.Contents.TabRight Legs"),
tabScale = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabList.Contents.TabScale"),
tabSkinTone = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabList.Contents.TabSkin Tone"),
tabTorsos = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabList.Contents.TabTorsos"),
},
animationTabs = {
tabClimb = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabListContainer.TabList.Contents.TabClimb Animations"),
tabFall = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabListContainer.TabList.Contents.TabFall Animations"),
tabIdle = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabListContainer.TabList.Contents.TabIdle Animations"),
tabJump = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabListContainer.TabList.Contents.TabJump Animations"),
tabRun = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabListContainer.TabList.Contents.TabRun Animations"),
tabSwim = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabListContainer.TabList.Contents.TabSwim Animations"),
tabWalk = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabListContainer.TabList.Contents.TabWalk Animations"),
},
animationTabs_portrait = {
tabClimb = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabList.Contents.TabClimb Animations"),
tabFall = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabList.Contents.TabFall Animations"),
tabIdle = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabList.Contents.TabIdle Animations"),
tabJump = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabList.Contents.TabJump Animations"),
tabRun = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabList.Contents.TabRun Animations"),
tabSwim = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabList.Contents.TabSwim Animations"),
tabWalk = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabList.Contents.TabWalk Animations"),
},
outfitsTabs = {
tabOutfits = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabListContainer.TabList.Contents.TabOutfits"),
},
outfitsTabs_portrait = {
tabOutfits = XPath.new("game.CoreGui.ScreenGui.RootGui.Frame.TabList.Contents.TabOutfits"),
},
getAvatarTabDetail = function(root)
return{
text = XPath.new("TextLabel", root),
}
end,
getGameListDetail = function(root)
return{
dropDownText = XPath.new("Contents.Scroller.scrollingFrame.Content.TopSection.DropDown.Text", root),
gameTitle = XPath.new("Contents.Scroller.scrollingFrame.Content.TopSection.Title", root),
gameCard = XPath.new("Contents.Scroller.scrollingFrame.Content.*.1.*[.ClassName = Frame]", root),
}
end,
getListPickerItem = function(root)
return {
Text = XPath.new("ImageAndText.Text", root)
}
end,
getGameCategoryDetail = function(root)
return {
title = XPath.new("Title.Title", root),
seeAllButton = XPath.new("Title.Spacer.Button.Text", root),
carousel = XPath.new("Carousel", root),
carouselItem = XPath.new("Carousel.*", root),
}
end,
getGameCardDetailFromCarouselItem = function(root)
return {
gameButton = XPath.new("GameButton", root),
playerCount = XPath.new("GameButton.GameInfo.PlayerCount", root),
title = XPath.new("GameButton.GameInfo.Title", root),
icon = XPath.new("GameButton.Icon", root),
}
end,
filterBy = function(container, relativePath, property, value)
local rootPath = container:copy()
local key = "."..property
if relativePath ~= nil then
key = "." .. relativePath:toString() .. key
end
local filter = {{key = key, value = value}}
rootPath:mergeFilter(rootPath:size(), filter)
return rootPath
end
}
return MobileAppElements
@@ -0,0 +1,78 @@
local PageNavigation = {}
PageNavigation.__index = PageNavigation
local Element = require(game.CoreGui.RobloxGui.Modules.Rhodium.Element)
local MobileAppElements = require(game.CoreGui.RobloxGui.Modules.RhodiumTest.Common.MobileAppElements)
local RobloxEventSimulator = require(game.CoreGui.RobloxGui.Modules.Rhodium.RobloxEventSimulator)
function PageNavigation:gotoAvatarPage()
-- in changelist 232625, they introduced a flag "useWebPageWrapperForGameDetails"
-- it will disable tool bar button for 1 second after click gamecard.
wait(1.5)
-- in studio we can navigate to different page by click on bottom bar
-- in android devices, their is no lua bottom bar, we navigate by simulating roblox event.
if game:GetService("RunService"):isStudio() then
Element.new(MobileAppElements.avatarButton):click()
else
RobloxEventSimulator.gotoPage(RobloxEventSimulator.Enums.pageAvatarEditor)
end
wait(0.5)
end
function PageNavigation:gotoHomePage()
if game:GetService("RunService"):isStudio() then
Element.new(MobileAppElements.homeButton):click()
else
RobloxEventSimulator.gotoPage(RobloxEventSimulator.Enums.pageHome)
end
wait(0.5)
end
function PageNavigation:gotoChatPage()
if game:GetService("RunService"):isStudio() then
Element.new(MobileAppElements.chatButton):click()
else
RobloxEventSimulator.gotoPage(RobloxEventSimulator.Enums.pageChat)
end
wait(0.5)
end
function PageNavigation:gotoGamesPage()
if game:GetService("RunService"):isStudio() then
Element.new(MobileAppElements.gamesButton):click()
else
RobloxEventSimulator.gotoPage(RobloxEventSimulator.Enums.pageGames)
end
wait(0.5)
end
function PageNavigation:gotoMorePage()
if game:GetService("RunService"):isStudio() then
Element.new(MobileAppElements.moreButton):click()
else
RobloxEventSimulator.gotoPage(RobloxEventSimulator.Enums.pageMore)
end
wait(0.5)
end
function PageNavigation:gotoCatalogPage()
if game:GetService("RunService"):isStudio() then
Element.new(MobileAppElements.catalogButton):click()
else
--TODO
--RobloxEventSimulator.gotoPage(RobloxEventSimulator.Enums.pageCatalog)
end
wait(0.5)
end
function PageNavigation:gotoFriendPage()
if game:GetService("RunService"):isStudio() then
Element.new(MobileAppElements.friendsButton):click()
else
--TODO
--RobloxEventSimulator.gotoPage(RobloxEventSimulator.Enums.pageFriends)
end
wait(0.5)
end
return PageNavigation
@@ -0,0 +1,248 @@
--to make ScriptAnalyzer Happy
describe = nil
step = nil
expect = nil
include = nil
-------------------------------
local Element = require(game.CoreGui.RobloxGui.Modules.Rhodium.Element)
local MobileAppElements = require(game.CoreGui.RobloxGui.Modules.RhodiumTest.Common.MobileAppElements)
local RobloxEventSimulator = require(game.CoreGui.RobloxGui.Modules.Rhodium.RobloxEventSimulator)
return function()
describe.protected("Test Avatar", function()
local function getProjectedPosition(position)
local Camera = game.workspace.Camera
local p = Camera.CFrame:pointToObjectSpace(position)
local ly = math.tan(Camera.FieldOfView / 2 / 180 * math.pi)
local aspect = Camera.ViewportSize.X / Camera.ViewportSize.Y
local ry = (p.Y / -p.Z) / ly
local rx = (p.X / -p.Z) / (ly * aspect)
return Vector3.new(rx, ry, p.Z)
end
local function similar(v1, v2, limit)
return (v1 - v2).Magnitude < limit
end
local function gotoAvatarPage()
-- in changelist 232625, they introduced a flag "useWebPageWrapperForGameDetails"
-- it will disable tool bar button for 1 second after click gamecard.
wait(2)
-- in studio we can navigate to different page by click on bottom bar
-- in android devices, their is no lua bottom bar, we navigate by simulating roblox event.
if game:GetService("RunService"):isStudio() then
Element.new(MobileAppElements.avatarButton):click()
else
RobloxEventSimulator.gotoPage(RobloxEventSimulator.Enums.pageAvatarEditor)
end
end
local function loadAvatarPage()
step("click avatar button on tool bar for studio", gotoAvatarPage)
step("should have character ", function()
assert(MobileAppElements.character:waitForFirstInstance(), "did not find character")
end)
step("should have r6 r15 switch ", function()
assert(MobileAppElements.r6r15Switch:waitForFirstInstance(), "did not find r6 r15 switch")
end)
step("should have full screen button", function()
assert(MobileAppElements.fullViewButton:waitForFirstInstance(), "did not find full screen button")
end)
step("should have tab button", function()
assert(MobileAppElements.selectTabButton:waitForFirstInstance(), "did not find tab button")
end)
step("should have page title should be \"Avatar\"", function()
assert(MobileAppElements.pageName:waitForFirstInstance().Text == "Avatar", "page name is not Avatar")
wait(1)
end)
end
local function fullViewButtonInScreen()
local button = MobileAppElements.fullViewButton:waitForFirstInstance()
assert(button)
local topLeftPos = button.AbsolutePosition()
local bottomRightPos = topLeftPos + button.AbsoluteSize()
local screenSize = game.workspace.Camera.ViewportSize
local minX, minY, maxX, maxY = 0, 0, screenSize.X, screenSize.Y
local topBar = MobileAppElements.topBar:waitForFirstInstance()
assert(topBar)
minY = math.max(minY, topBar.AbsolutePosition.Y + topBar.AbsoluteSize.Y)
local rightFrame = MobileAppElements.rightFrame:waitForFirstInstance()
assert(rightFrame)
maxX = math.min(maxX, rightFrame.AbsolutePosition.X)
local bottomBar = MobileAppElements.topBarContent:waitForFirstInstance()
assert(bottomBar)
maxY = math.min(maxY, bottomBar.AbsolutePosition.Y)
assert(topLeftPos.X > minX)
assert(topLeftPos.Y > minY)
assert(bottomRightPos.X < maxX)
assert(bottomRightPos.Y < maxY)
end
local function testFullViewButtonLandScape()
local button = MobileAppElements.fullViewButton:waitForFirstInstance()
local rightFrame = MobileAppElements.rightFrame:waitForFirstInstance()
assert(button)
assert(rightFrame)
fullViewButtonInScreen()
assert(similar(rightFrame.AbsolutePostion.X), game.workspace.Camera.ViewportSize.X / 2, 100)
local element = Element.new(button)
element:click()
wait(2) -- wait to finish animation
fullViewButtonInScreen()
assert(similar(rightFrame.AbsolutePostion.X), game.workspace.Camera.ViewportSize.X, 20)
element:click()
wait(2) -- wait to finish animation
fullViewButtonInScreen()
assert(similar(rightFrame.AbsolutePostion.X), game.workspace.Camera.ViewportSize.X / 2, 100)
end
local function waitForCloseButton()
return MobileAppElements.closeTabButton:waitForInstance()
end
local function waitForCloseButtonDisappear()
return MobileAppElements.closeTabButton:waitForDisappear()
end
local function instanceInScreen(instance)
local pos = getProjectedPosition(instance.Position)
return((pos.Z < 0)
and (pos.X > -1 and pos.X < 1)
and (pos.Y > -1 and pos.Y < 1))
end
local function checkAvatarInScreen()
local character = MobileAppElements.character:waitForFirstInstance()
assert(character)
local rootPart = character.HumanoidRootPart
local head = character.Head
assert(rootPart)
assert(head)
assert(instanceInScreen(rootPart), "avatar is not inside of screen")
end
local function gotoTab(tab)
assert(waitForCloseButtonDisappear() == true)
Element.new(MobileAppElements.selectTabButton):click()
Element.new(tab):click()
assert(waitForCloseButtonDisappear() == true)
end
local function forEachTab(tab, subTabs)
step("should go to that tab after click", function()
gotoTab(tab)
end)
step("tab should be yellow after click and character should be aways in screen", function()
for name, path in pairs(subTabs) do
print(string.format("testing avatar tab %q", name))
local instance
instance = path:waitForFirstInstance()
assert(instance)
Element.new(instance):click()
wait(0.2)
assert(instance.BackgroundColor3.r ~= 255)
checkAvatarInScreen()
end
end)
end
local function testAllTabs_Portrait()
describe.protected("all \"recent\" tabs should work", function()
forEachTab(MobileAppElements.groupTabs_portrait.recentButton, MobileAppElements.recentTabs_portrait)
end)
describe.protected("all \"clothing\" tabs should work", function()
forEachTab(MobileAppElements.groupTabs_portrait.clothingButton, MobileAppElements.clothingTabs_portrait)
end)
describe.protected("all \"body\" tabs should work", function()
forEachTab(MobileAppElements.groupTabs_portrait.bodyButton, MobileAppElements.bodyTabs_portrait)
end)
describe.protected("all \"anomation\" tabs should work", function()
forEachTab(MobileAppElements.groupTabs_portrait.animationButton, MobileAppElements.animationTabs_portrait)
end)
describe.protected("all \"outfits\" tabs should work", function()
forEachTab(MobileAppElements.groupTabs_portrait.outfitsButton, MobileAppElements.outfitsTabs_portrait)
end)
end
local function testAllTabs()
describe.protected("all \"recent\" tabs should work", function()
forEachTab(MobileAppElements.groupTabs.recentButton, MobileAppElements.recentTabs)
end)
describe.protected("all \"clothing\" tabs should work", function()
forEachTab(MobileAppElements.groupTabs.clothingButton, MobileAppElements.clothingTabs)
end)
describe.protected("all \"body\" tabs should work", function()
forEachTab(MobileAppElements.groupTabs.bodyButton, MobileAppElements.bodyTabs)
end)
describe.protected("all \"anomation\" tabs should work", function()
forEachTab(MobileAppElements.groupTabs.animationButton, MobileAppElements.animationTabs)
end)
describe.protected("all \"outfits\" tabs should work", function()
forEachTab(MobileAppElements.groupTabs.outfitsButton, MobileAppElements.outfitsTabs)
end)
end
local function isR15()
local switchFrame = MobileAppElements.r6r15SwitchFrame:waitForFirstInstance()
assert(switchFrame)
local switch = MobileAppElements.r6r15Switch:waitForFirstInstance()
assert(switch)
local frameCenter = Element.new(switchFrame):getCenter()
local switchCenter = Element.new(switch):getCenter()
return switchCenter.X > frameCenter.X
end
local function switchToR15()
if isR15() == true then return end
Element.new(MobileAppElements.r6r15Switch):click()
wait(1) -- it takes a long time to switch
assert(isR15())
end
local function switchToR6()
if isR15() == false then return end
Element.new(MobileAppElements.r6r15Switch):click()
wait(1) -- it takes a long time to switch
assert(not isR15())
end
local function switchCharacter()
if isR15() == true then
switchToR6()
else
switchToR15()
end
end
local function switchTest()
if isR15() == true then
switchToR6()
switchToR15()
else
switchToR15()
switchToR6()
end
print("current avatar style is " .. (isR15() and "R15" or "R6"))
end
describe("should be able to goto avatar page by navigation button", loadAvatarPage)
step("switch should work", switchTest)
describe.protected("test all tabs", testAllTabs)
-- the R6 avatar can move out of screen in studio mode.
if false then
step("should switch to another character", switchCharacter)
describe.protected("test all tabs after character switched", testAllTabs)
step("should switch back to old character", switchCharacter)
end
end)
end
@@ -0,0 +1,19 @@
--to make ScriptAnalyzer Happy
describe = nil
step = nil
expect = nil
include = nil
-------------------------------
local PageNavigation = require(game.CoreGui.RobloxGui.Modules.RhodiumTest.Common.PageNavigation)
return function()
step("Switching pages", function()
PageNavigation.gotoGamesPage()
PageNavigation.gotoCatalogPage()
PageNavigation.gotoAvatarPage()
PageNavigation.gotoFriendPage()
PageNavigation.gotoChatPage()
PageNavigation.gotoMorePage()
PageNavigation.gotoHomePage()
end)
end
@@ -0,0 +1,79 @@
--to make ScriptAnalyzer Happy
describe = nil
step = nil
expect = nil
include = nil
-------------------------------
local Element = require(game.CoreGui.RobloxGui.Modules.Rhodium.Element)
local MobileAppElements = require(game.CoreGui.RobloxGui.Modules.RhodiumTest.Common.MobileAppElements)
local VirtualInput = require(game.CoreGui.RobloxGui.Modules.Rhodium.VirtualInput)
return function()
describe.protected("game Button should zoom out on its center point when touch and hold it, and resume after release",
function()
local myRecentEntry =
MobileAppElements.filterBy(MobileAppElements.gameCategoryEntry, MobileAppElements.getGameCategoryDetail().title,
"Text", "My Recent"
)
local recentInstance = nil
local gameFrame = nil
local gameButton = nil
local element = nil
local center = nil
local oldFrameSize = nil
local oldGameButtonSize = nil
local oldGameButtonCenter = nil
step("should find the carousel and game buttons", function()
recentInstance = myRecentEntry:waitForFirstInstance()
expect(recentInstance).to.be.ok()
gameFrame = recentInstance.Carousel:waitForChild("1")
gameButton = gameFrame.GameButton
element = Element.new(gameButton)
--bring it in screen, if not.
element:centralize()
wait(0.5)
center = element:getCenter()
oldFrameSize = gameFrame.AbsoluteSize
oldGameButtonSize = gameButton.AbsoluteSize
oldGameButtonCenter = element:getCenter()
end)
local function similar(v1, v2, limit)
return (v1 - v2).Magnitude < limit
end
step("game Button should zoom out on its center point when touch and hold it", function()
VirtualInput.mouseLeftDown(center)
VirtualInput.touchStart(center)
wait(1) -- wait for animation finish
local downFrameSize = gameFrame.AbsoluteSize
local downGameButtonSize = gameButton.AbsoluteSize
local downGameButtonCenter = Element.new(gameButton):getCenter()
assert(similar(oldFrameSize, downFrameSize, 2))
assert(oldGameButtonSize.Magnitude > downGameButtonSize.Magnitude)
assert(not similar(oldGameButtonSize, downGameButtonSize, 4))
assert(similar(oldGameButtonCenter, downGameButtonCenter, 2))
VirtualInput.mouseLeftUp(center)
VirtualInput.touchStop(center)
end)
step("game Button should resume after release", function()
wait(1) -- wait for animation finish
local upFrameSize = gameFrame.AbsoluteSize
local upGameButtonSize = gameButton.AbsoluteSize
local upGameButtonCenter = Element.new(gameButton):getCenter()
assert(similar(oldFrameSize, upFrameSize, 2))
assert(similar(oldGameButtonSize, upGameButtonSize, 2))
assert(similar(oldGameButtonCenter, upGameButtonCenter, 2))
end)
end)
end
@@ -0,0 +1,57 @@
--to make ScriptAnalyzer Happy
describe = nil
step = nil
expect = nil
include = nil
-------------------------------
local Element = require(game.CoreGui.RobloxGui.Modules.Rhodium.Element)
local MobileAppElements = require(game.CoreGui.RobloxGui.Modules.RhodiumTest.Common.MobileAppElements)
local RobloxEventSimulator = require(game.CoreGui.RobloxGui.Modules.Rhodium.RobloxEventSimulator)
local RunService = game:GetService("RunService")
return function()
describe.protected("Home Page Should Load", function()
if RunService:IsStudio() then
step("should have tool bar", function()
expect(MobileAppElements.topBar:setWait(10):waitForFirstInstance()).to.be.ok()
end)
step("all tool bar buttons should be ok", function()
expect(MobileAppElements.avatarButton:setWait(10):waitForFirstInstance()).to.be.ok()
expect(MobileAppElements.catalogButton:waitForFirstInstance()).to.be.ok()
expect(MobileAppElements.chatButton:waitForFirstInstance()).to.be.ok()
expect(MobileAppElements.friendsButton:waitForFirstInstance()).to.be.ok()
expect(MobileAppElements.gamesButton:waitForFirstInstance()).to.be.ok()
expect(MobileAppElements.homeButton:waitForFirstInstance()).to.be.ok()
expect(MobileAppElements.moreButton:waitForFirstInstance()).to.be.ok()
end)
end
step("should be able to goto home page", function()
if RunService:IsStudio() then
Element.new(MobileAppElements.homeButton):click()
else
RobloxEventSimulator.gotoPage(RobloxEventSimulator.Enums.pageHome)
end
expect(MobileAppElements.pageName:waitForFirstInstance().Text).to.equal("Home")
end)
step("scroling frame should be at the top when entered home page", function()
expect(MobileAppElements.verticalScrollingFrame:setWait(10):waitForFirstInstance().CanvasPosition.Y).to.equal(0)
end)
step("should have [My Recent] [Friend Activity] and Recommended carousels", function()
--"My Recent", only if the account played some games
--"Friend Activity", only if the account has friends
local carouselNames = {"Recommended"}
local function checkCarousel(name)
expect(MobileAppElements.filterBy(MobileAppElements.gameCategoryEntry,
MobileAppElements.getGameCategoryDetail().title, "Text", name):waitForFirstInstance()).to.be.ok()
end
for _, name in ipairs(carouselNames) do
checkCarousel(name)
end
end)
end)
end
@@ -0,0 +1,19 @@
--to make ScriptAnalyzer Happy
describe = nil
step = nil
expect = nil
include = nil
-------------------------------
local Element = require(game.CoreGui.RobloxGui.Modules.Rhodium.Element)
local MobileAppElements = require(game.CoreGui.RobloxGui.Modules.RhodiumTest.Common.MobileAppElements)
local VirtualInput = require(game.CoreGui.RobloxGui.Modules.Rhodium.VirtualInput)
return function()
step("mouse wheel should be able to scroll the vertical scrollframe", function()
local element = Element.new(MobileAppElements.verticalScrollingFrame)
wait(0.5)
VirtualInput.mouseWheel(element:getCenter(), 2)
wait(0.5) -- wait for one frame to update GUIs.
assert(MobileAppElements.verticalScrollingFrame:waitForFirstInstance().CanvasPosition.Y > 0)
end)
end
@@ -0,0 +1,141 @@
describe = nil
step = nil
expect = nil
include = nil
local Modules = game:GetService("CoreGui").RobloxGui.Modules
local Constants = require(Modules.LuaApp.Constants)
local Element = require(Modules.Rhodium.Element)
local MobileAppElements = require(Modules.RhodiumTest.Common.MobileAppElements)
local PageNavigation = require(Modules.RhodiumTest.Common.PageNavigation)
local VirtualInput = require(Modules.Rhodium.VirtualInput)
local ADD_FRIEND_BUTTON_HEIGHT = 132
local FRIEND_CAROUSEL = {
Username_Font = Enum.Font.SourceSansLight,
Username_TextColor = Constants.Color.GRAY1,
Phone = {
WIDTH = 115,
HEIGHT = 153,
ICON_SIZE = 84,
},
Tablet = {
WIDTH = 105,
HEIGHT = 161,
ICON_SIZE = 90,
}
}
return function()
local ADD_FRIENDS_ICON = "rbxasset://textures/ui/LuaApp/icons/ic-add.png"
local ADD_FRIENDS_ICON_PRESSED = "rbxasset://textures/ui/LuaApp/icons/ic-add-down.png"
describe.protected("Homepage People List Rhodium Test", function()
-- func step is executed step by step
local peopleList
local addFriendsButton
local firstFriendCarousel
step("Navigate to homepage", function()
-- make sure we are at homepage now
PageNavigation.gotoHomePage()
end)
step("Load elements", function()
peopleList = MobileAppElements.homePagePeopleList:waitForFirstInstance()
assert(peopleList, "create people list unsuccessfully")
-- AddFriendsButton
addFriendsButton = peopleList["1"]
assert(addFriendsButton, "create AddFriendsButton unsuccessfully")
-- First friend carousel
firstFriendCarousel = peopleList["2"]
assert(firstFriendCarousel)
end)
describe.protected("Test AddFriendsButton on People List", function()
step.protected("AddFriendsButton layout", function()
-- check button size
assert(addFriendsButton.AbsoluteSize.x == Constants.PeopleList.ADD_FRIENDS_FRAME_WIDTH, "AddFriendsButton width is wrong")
assert(addFriendsButton.AbsoluteSize.y == ADD_FRIEND_BUTTON_HEIGHT, "AddFriendsButton height is wrong")
end)
step.protected("AddFriendsButton click effect", function()
-- check button asset
assert(addFriendsButton.AddButton.Image == ADD_FRIENDS_ICON, "AddFriendsButton's image is wrong")
-- check button clicked asset
local centerPoint = Element.new(addFriendsButton):getCenter()
VirtualInput.touchStart(centerPoint)
-- wait one frame
wait()
assert(addFriendsButton.AddButton.Image == ADD_FRIENDS_ICON_PRESSED, "AddFriendsButton's pressed image is wrong")
VirtualInput.touchStop(centerPoint)
wait()
assert(addFriendsButton.AddButton.Image == ADD_FRIENDS_ICON, "AddFriendsButton's image is wrong")
end)
end)
describe.protected("Test user carousel on people list", function()
step.protected("User carousel layout", function()
local headIcon = firstFriendCarousel.ThumbnailFrame.Thumbnail.Image
local userName = firstFriendCarousel.ThumbnailFrame.Thumbnail.Username
assert(headIcon, "can not find head icon")
assert(userName, "can not find username label")
-- Tablet
if firstFriendCarousel.AbsoluteSize.x == FRIEND_CAROUSEL.Tablet.WIDTH then
assert(firstFriendCarousel.AbsoluteSize.y == FRIEND_CAROUSEL.Tablet.HEIGHT)
assert(headIcon.AbsoluteSize.x == FRIEND_CAROUSEL.Tablet.ICON_SIZE)
else
-- Phone
assert(firstFriendCarousel.AbsoluteSize.x == FRIEND_CAROUSEL.Phone.WIDTH)
assert(firstFriendCarousel.AbsoluteSize.y == FRIEND_CAROUSEL.Phone.HEIGHT)
assert(headIcon.AbsoluteSize.x == FRIEND_CAROUSEL.Phone.ICON_SIZE)
end
assert(userName.TextColor3 == FRIEND_CAROUSEL.Username_TextColor)
assert(userName.Font == FRIEND_CAROUSEL.Username_Font)
end)
step.protected("User carousel click event", function()
-- click one of the friend carousel
Element.new(firstFriendCarousel):click()
local peopleListContextualMenu = MobileAppElements.peopleListContextualMenu:waitForFirstInstance()
assert(peopleListContextualMenu, "create peopleList Contextual menu unsuccessfully")
wait(0.5)
Element.new(peopleListContextualMenu):click()
wait(1)
peopleListContextualMenu = MobileAppElements.peopleListContextualMenu:waitForFirstInstance()
assert(peopleListContextualMenu == nil, "can not close people list contextual menu")
end)
end)
describe.protected("Test scrolling frame of people list", function()
step.protected("People List Scrolling test", function ()
local screenWidth = peopleList.AbsoluteSize.x
local peopleListContentSize = peopleList.CanvasSize.X.Offset
local startPoint = Element.new(peopleList):getCenter()
-- Move to left 100 pixel
-- This number should be larger.
-- if it's too small, the scrolling frame would be scrolled
local endPoint = Vector2.new(startPoint.X - 100, startPoint.Y)
VirtualInput.swipe(startPoint, endPoint, 0.2)
wait(0.5)
if peopleListContentSize > screenWidth then
assert(peopleList.CanvasPosition.X > 0, "Can not move people list")
else
assert(peopleList.CanvasPosition.X == 0, "Can move a non-scrollable people list")
end
end)
end)
end)
end
@@ -0,0 +1,135 @@
--to make ScriptAnalyzer Happy
describe = nil
step = nil
expect = nil
include = nil
-------------------------------
local Element = require(game.CoreGui.RobloxGui.Modules.Rhodium.Element)
local MobileAppElements = require(game.CoreGui.RobloxGui.Modules.RhodiumTest.Common.MobileAppElements)
--"My Recent", only if the account played some games
--"Friend Activity", only if the account has friends
local popupMenuText = {"Featured", "Friend Activity", "My Recent", "Popular",
"Popular Near You", "Recommended", "Top Earning", "Top Rated"}
local function getMenuItem(text)
return MobileAppElements.filterBy(MobileAppElements.listPickerItem,
MobileAppElements.getListPickerItem().Text, "Text", text)
end
local function clickMenu(text)
local path = getMenuItem(text)
local instance = path:waitForFirstInstance()
assert(instance)
Element.new(instance):click()
end
local function checkAllMenuExists()
for _, text in ipairs(popupMenuText) do
local path = getMenuItem(text)
assert(path:waitForFirstInstance())
end
end
local function isMenuOpen()
return MobileAppElements.listPicker:getFirstInstance() ~= nil
end
local function getCurrentListDetail()
--because it takes some time to set old screen as disabled
wait(0.2)
local homePath = MobileAppElements.homeScreen
homePath = MobileAppElements.filterBy(homePath, nil, "Enabled", true)
local result = homePath:waitForDisappear()
assert(result)
local currentGameList = MobileAppElements.currentGameList
local currentGameListInstance = currentGameList:waitForFirstInstance()
assert(currentGameListInstance)
assert(currentGameListInstance.Name ~= "Home")
local gameListDetail = MobileAppElements.getGameListDetail(currentGameListInstance)
return gameListDetail
end
local function openMenu()
if isMenuOpen() then return end
local element = Element.new(getCurrentListDetail().dropDownText)
element:click()
end
local function closeMenu()
if not isMenuOpen() then return end
local element = Element.new(getCurrentListDetail().dropDownText)
element:click()
end
local function clickSeeAll(category)
local recommendedEntry = MobileAppElements.filterBy(MobileAppElements.gameCategoryEntry,
MobileAppElements.getGameCategoryDetail().title, "Text", category)
local recommendedEntryInstance = recommendedEntry:waitForFirstInstance()
assert(recommendedEntryInstance)
local seeAllButton = MobileAppElements.getGameCategoryDetail(recommendedEntryInstance).seeAllButton
local seeAllButtonInstance = seeAllButton:waitForFirstInstance()
assert(seeAllButtonInstance)
Element.new(seeAllButtonInstance):click()
end
local function checkIsList(name)
print("checking game list: ", name)
local gameListDetail = getCurrentListDetail()
local gameTitle = MobileAppElements.filterBy(gameListDetail.gameTitle, nil, "Text", name)
local gameTitleInstance = gameTitle:waitForFirstInstance()
assert(gameTitleInstance)
assert(gameListDetail.dropDownText:waitForFirstInstance())
assert(gameListDetail.gameTitle:waitForFirstInstance())
assert(gameListDetail.gameCard:waitForFirstInstance())
assert(gameListDetail.dropDownText:waitForFirstInstance().Text == name)
assert(gameListDetail.gameTitle:waitForFirstInstance().Text == name)
end
local function canNavigateToAllMenu()
for _, text in ipairs(popupMenuText) do
step(string.format("should be able to navigate to game list %q ", text), function()
openMenu()
clickMenu(text)
checkIsList(text)
end)
end
end
local function gameCardExist()
local gameListDetail = getCurrentListDetail()
local gameCard = MobileAppElements.filterBy(gameListDetail.gameCard,
MobileAppElements.getGameCardDetailFromCarouselItem().gameButton, "ClassName", "TextButton")
local instance = gameCard:waitForFirstInstance()
assert(instance)
end
return function()
describe.protected("should be able to navigate to all game lists", function()
step("should be able to find the SeeAll button on home page and click it", function()
clickSeeAll("Recommended")
end)
step("should open recommended page after click see all", function()
checkIsList("Recommended")
end)
step("the pop up menu should not show up when switch to that page", function()
assert(isMenuOpen() == false)
end)
step("should have some game cards on this page", function()
gameCardExist()
end)
describe("should be able to navigate to every game list by drop down menu", function()
include(canNavigateToAllMenu)
end)
end)
end
@@ -0,0 +1,37 @@
--to make ScriptAnalyzer Happy
describe = nil
step = nil
expect = nil
include = nil
-------------------------------
local Test = game.CoreGui.RobloxGui.Modules.Test4R
local startTest = require(Test.startTest)
local TextReporter = require(Test.Reporters.TextReporter)
local TeamCityReporter = require(Test.Reporters.TeamCityReporter)
local reporter = _G["TEAMCITY"] and TeamCityReporter or TextReporter
local HomePageLoad = require(script.Parent.Modules.HomePageLoad)
local HomePageScroll = require(script.Parent.Modules.HomePageScroll)
local GameGardZoom = require(script.Parent.Modules.GameGardZoom)
local NavigateToAllGameLists = require(script.Parent.Modules.NavigateToAllGameLists)
local AvatarTest = require(script.Parent.Modules.AvatarTest)
local AllTabsSwitch = require(script.Parent.Modules.BVT.AllTabsSwitch)
local function test()
describe.protected("Mobile Place Rhodium Test", function()
include(AllTabsSwitch)
include(HomePageLoad)
include(HomePageScroll)
include(GameGardZoom)
-- gametest1 do not have to much games for testting.
-- include(NavigateToAllGameLists)
include(AvatarTest)
end)
end
function run()
startTest(test, reporter)
end
return run
@@ -0,0 +1,26 @@
--to make ScriptAnalyzer Happy
describe = nil
step = nil
expect = nil
include = nil
-------------------------------
local Test = game.CoreGui.RobloxGui.Modules.Test4R
local startTest = require(Test.startTest)
local TextReporter = require(Test.Reporters.TextReporter)
local TeamCityReporter = require(Test.Reporters.TeamCityReporter)
local reporter = _G["TEAMCITY"] and TeamCityReporter or TextReporter
local AllTabsSwitch = require(script.Parent.Modules.BVT.AllTabsSwitch)
local function test()
describe.protected("Mobile Place Rhodium Basic Functional Test", function()
--include(YourFunctionalTest)
end)
end
function run()
startTest(test, reporter)
end
return run
@@ -0,0 +1,26 @@
--to make ScriptAnalyzer Happy
describe = nil
step = nil
expect = nil
include = nil
-------------------------------
local Test = game.CoreGui.RobloxGui.Modules.Test4R
local startTest = require(Test.startTest)
local TextReporter = require(Test.Reporters.TextReporter)
local TeamCityReporter = require(Test.Reporters.TeamCityReporter)
local reporter = _G["TEAMCITY"] and TeamCityReporter or TextReporter
local AllTabsSwitch = require(script.Parent.Modules.BVT.AllTabsSwitch)
local function test()
describe.protected("Mobile Place Rhodium Basic Verification Test", function()
include(AllTabsSwitch)
end)
end
function run()
startTest(test, reporter)
end
return run
@@ -0,0 +1,26 @@
--to make ScriptAnalyzer Happy
describe = nil
step = nil
expect = nil
include = nil
-------------------------------
local Test = game.CoreGui.RobloxGui.Modules.Test4R
local startTest = require(Test.startTest)
local TextReporter = require(Test.Reporters.TextReporter)
local TeamCityReporter = require(Test.Reporters.TeamCityReporter)
local reporter = _G["TEAMCITY"] and TeamCityReporter or TextReporter
local AllTabsSwitch = require(script.Parent.Modules.BVT.AllTabsSwitch)
local function test()
describe.protected("Mobile Place Rhodium Comprehensive Test", function()
--include(YourComprehensiveTest)
end)
end
function run()
startTest(test, reporter)
end
return run
@@ -0,0 +1,12 @@
local Enums = {
Type = {
Describe = "Describe",
Step = "Step",
},
Modifier = {
Protect = "Protect",
Skip = "Skip"
},
}
return Enums
@@ -0,0 +1,89 @@
local TestService = game:GetService("TestService")
local Enums = require(script.Parent.Parent.Enums)
local TeamCityReporter = {}
local function teamCityEscape(str)
str = string.gsub(str, "([]|'[])","|%1")
str = string.gsub(str, "\r", "|r")
str = string.gsub(str, "\n", "|n")
return str
end
local function teamCityEnterSuite(suiteName)
return string.format("##teamcity[testSuiteStarted name='%s']", teamCityEscape(suiteName))
end
local function teamCityLeaveSuite(suiteName)
return string.format("##teamcity[testSuiteFinished name='%s']", teamCityEscape(suiteName))
end
local function teamCityEnterCase(caseName)
return string.format("##teamcity[testStarted name='%s']", teamCityEscape(caseName))
end
local function teamCityLeaveCase(caseName)
return string.format("##teamcity[testFinished name='%s']", teamCityEscape(caseName))
end
local function teamCityFailCase(caseName, errorMessage)
return string.format("##teamcity[testFailed name='%s' message='%s']",
teamCityEscape(caseName), teamCityEscape(errorMessage))
end
local function reportNode(node, buffer, level)
buffer = buffer or {}
level = level or 0
if not node.testTouched then
return buffer
end
if node.type == Enums.Type.Describe then
table.insert(buffer, teamCityEnterSuite(node.text))
for _, child in ipairs(node.children) do
reportNode(child, buffer, level + 1)
end
table.insert(buffer, teamCityLeaveSuite(node.text))
elseif node.type == Enums.Type.Step then
table.insert(buffer, teamCityEnterCase(node.text))
if not node.testSuccess then
table.insert(buffer, teamCityFailCase(node.text, node.errorMessage))
end
table.insert(buffer, teamCityLeaveCase(node.text))
end
end
local function treeToString(testNode)
local result = {}
for _, child in ipairs(testNode.children) do
reportNode(child, result)
end
return table.concat(result, "\n")
end
function TeamCityReporter.report(results)
local resultBuffer = {
"Test results:",
treeToString(results.testNode),
string.format(
"%d passed, %d failed, %d skipped",
results.successCount,
results.failureCount,
results.skippedCount
)
}
print(table.concat(resultBuffer, "\n"))
if results.failureCount > 0 then
print(("%d test nodes reported failures."):format(results.failureCount))
end
if #results.errors > 0 then
print("Errors reported by tests:\n")
for _, message in ipairs(results.errors) do
TestService:Error(message)
print("")
end
end
end
return TeamCityReporter
@@ -0,0 +1,49 @@
local TestService = game:GetService("TestService")
local TextReporter = {}
local INDENT = (" "):rep(3)
local function treeToString(testNode)
local result = {}
local function callback(node, level)
local symbol = node.testTouched and (node.testSuccess and "+" or "-") or "~"
local line = ("%s[%s] %s"):format(
INDENT:rep(level),
symbol,
node.text
)
table.insert(result, line)
end
testNode:visit(callback)
return table.concat(result, "\n")
end
function TextReporter.report(results)
local resultBuffer = {
"Test results:",
treeToString(results.testNode),
string.format(
"%d passed, %d failed, %d skipped",
results.successCount,
results.failureCount,
results.skippedCount
)
}
print(table.concat(resultBuffer, "\n"))
if results.failureCount > 0 then
print(("%d test nodes reported failures."):format(results.failureCount))
end
if #results.errors > 0 then
print("Errors reported by tests:\n")
for _, message in ipairs(results.errors) do
TestService:Error(message)
print("")
end
end
end
return TextReporter
@@ -0,0 +1,39 @@
local TestNode = {}
TestNode.__index = TestNode
function TestNode.new(text, type, modifiers)
local self = {
parent = nil,
children = {},
text = text,
type = type,
modifiers = modifiers or {},
callback = nil,
testSuccess = true,
testTouched = false,
loadSuccess = true,
errorMessage = "",
}
setmetatable(self, TestNode)
return self
end
function TestNode:addChild(node)
node.parent = self
table.insert(self.children, node)
end
function TestNode:visit(callback, level)
level = level or 0
callback(self, level);
for _, child in ipairs(self.children) do
child:visit(callback, level + 1)
end
end
function TestNode:isRoot()
return self.parent == nil
end
return TestNode
@@ -0,0 +1,70 @@
local Enums = require(script.Parent.Enums)
local TestNode = require(script.Parent.TestNode)
local createModdableFunction = require(script.Parent.createModdableFunction)
local TestPlanner = {}
TestPlanner.__index = TestPlanner
local function doPlan(currentNode, method, env)
local currentEnv = getfenv(method)
for key, value in pairs(env) do
currentEnv[key] = value
end
local success, result = xpcall(method, function(err)
return err .. "\n" .. debug.traceback()
end)
if not success then
currentNode.loadSuccess = false
currentNode.errorMessage = result
end
end
local validModifiers = {
protected = Enums.Modifier.Protect,
skipped = Enums.Modifier.Skip,
}
local function createEnv(currentNode)
local function describe(modifiers, text, callback)
local testNode = TestNode.new(text, Enums.Type.Describe, modifiers)
currentNode:addChild(testNode)
currentNode = testNode
local success, result = xpcall(callback, function(err)
return err .. "\n" .. debug.traceback()
end)
if not success then
testNode.loadSuccess = false
testNode.errorMessage = result
end
currentNode = currentNode.parent
end
local function step(modifiers, text, callback)
local testNode = TestNode.new(text, Enums.Type.Step, modifiers)
testNode.callback = callback
currentNode:addChild(testNode)
end
local env = {}
env.describe = createModdableFunction(validModifiers, describe)
env.step = createModdableFunction(validModifiers, step)
env.skip = function()
currentNode.modifiers[Enums.Modifier.Skip] = true
end
env.include = function(method)
doPlan(currentNode, method, env)
end
return env
end
function TestPlanner.plan(method)
local currentNode = TestNode.new("Root", Enums.Type.Describe)
local env = createEnv(currentNode)
doPlan(currentNode, method, env)
return currentNode
end
return TestPlanner
@@ -0,0 +1,36 @@
local Enums = require(script.Parent.Enums)
local TestResult = {}
TestResult.__index = TestResult
function TestResult.getResult(testNode)
local result = {
testNode = testNode,
successCount = 0,
failureCount = 0,
skippedCount = 0,
errors = {}
}
local function callback(node, level)
if node.type == Enums.Type.Describe then
if not node.loadSuccess then
result.failureCount = result.failureCount + 1
table.insert(result.errors, node.errorMessage)
end
elseif node.type == Enums.Type.Step then
if not node.testTouched then
result.skippedCount = result.skippedCount + 1
elseif node.testSuccess then
result.successCount = result.successCount + 1
else
result.failureCount = result.failureCount + 1
table.insert(result.errors, node.errorMessage)
end
end
end
testNode:visit(callback)
return result
end
return TestResult
@@ -0,0 +1,72 @@
local CorePackages = game:GetService("CorePackages")
local Enums = require(script.Parent.Enums)
local Expectation = require(CorePackages.TestEZ).Expectation
local TestRunner = {}
local testEnv = {expect = Expectation.new}
local function assign(to, from)
for key, value in pairs(from) do
to[key] = value
end
end
local function setTestEnv(method)
assign(getfenv(method), testEnv)
end
local function run(testNode, protectionState)
if testNode.modifiers[Enums.Modifier.Skip] then
return
end
testNode.testTouched = true
if testNode.modifiers[Enums.Modifier.Protect] then
-- Update the protection value so that descendant nodes will reference this value
protectionState = {
failedNode = protectionState.failedNode
}
end
if testNode.type == Enums.Type.Describe then
for _, child in ipairs(testNode.children) do
run(child, protectionState)
end
elseif testNode.type == Enums.Type.Step then
if protectionState and protectionState.failedNode then
testNode.testSuccess = false
testNode.errorMessage = string.format(
"%q failed without execution, because %q failed",
testNode.text, protectionState.failedNode.text
)
else
setTestEnv(testNode.callback)
local success, result = pcall(testNode.callback)
if not success then
testNode.testSuccess = false
testNode.errorMessage = result
protectionState.failedNode = testNode
end
end
end
if not testNode.loadSuccess then
testNode.errorMessage = string.format("Error during planning: %q\n%s", testNode.text, testNode.errorMessage)
testNode.testSuccess = false
end
if testNode.parent and not testNode.testSuccess then
testNode.parent.testSuccess = false
end
end
function TestRunner.run(testNode)
-- protected by default, which means when one step failed, all steps after will be skipped by default
run(testNode, {
failedNode = nil
})
return testNode
end
return TestRunner
@@ -0,0 +1,39 @@
--[[createModdableFunction can be used to build a Moddable function in a chain style
for example:
validModifiers = {
protected = Enums.Modifier.Protect,
skipped = Enums.Modifier.Skip,
}
describe = createModdableFunction(validModifiers, callback)
describe.protected.skipped("description", function() end)
step.skipped("description", function() end)
]]
local function createModdableFunction(validModifiers, method, appliedModifiers)
if appliedModifiers == nil then
appliedModifiers = {}
end
return setmetatable({}, {
__call = function(_, ...)
return method(appliedModifiers, ...)
end,
__index = function(_, key)
if not validModifiers[key] then
error(("%q is not a valid modifier"):format(tostring(key)), 2)
end
local newAppliedModifiers = {}
for key, value in pairs(appliedModifiers) do
newAppliedModifiers[key] = value
end
newAppliedModifiers[validModifiers[key]] = true
return createModdableFunction(validModifiers, method, newAppliedModifiers)
end,
})
end
return createModdableFunction
@@ -0,0 +1,12 @@
local TestPlanner = require(script.Parent.TestPlanner)
local TestRunner = require(script.Parent.TestRunner)
local TestResult = require(script.Parent.TestResult)
local function startTest(method, reporter)
local testNode = TestPlanner.plan(method)
testNode = TestRunner.run(testNode)
local result = TestResult.getResult(testNode)
reporter.report(result)
end
return startTest