add gs
This commit is contained in:
@@ -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
|
||||
+248
@@ -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
|
||||
+19
@@ -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
|
||||
+79
@@ -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
|
||||
+57
@@ -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
|
||||
+19
@@ -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
|
||||
+141
@@ -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
|
||||
+135
@@ -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
|
||||
+37
@@ -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
|
||||
+26
@@ -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
|
||||
+26
@@ -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
|
||||
+26
@@ -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
|
||||
Reference in New Issue
Block a user