This commit is contained in:
lx
2026-07-06 14:01:11 -04:00
commit c4f97d729d
16468 changed files with 935321 additions and 0 deletions
@@ -0,0 +1,41 @@
stds.roblox = {
globals = {
"game"
},
read_globals = {
-- Roblox globals
"script",
-- Extra functions
"tick", "warn", "spawn",
"wait", "settings", "typeof", "delay",
-- Types
"Vector2", "Vector3",
"Color3",
"UDim", "UDim2",
"Rect",
"CFrame",
"Enum",
"Instance",
}
}
stds.testez = {
read_globals = {
"describe",
"it", "itFOCUS", "itSKIP",
"FOCUS", "SKIP", "HACK_NO_XPCALL",
"expect",
}
}
ignore = {
"212", -- unused arguments
}
std = "lua51+roblox"
files["**/*.spec.lua"] = {
std = "+testez",
}
@@ -0,0 +1,17 @@
{
"language": {
"mode": "noinfer"
},
"lint": {
"MultiLineStatement": "disabled",
"DeprecatedGlobal": "disabled",
"LocalShadow": "disabled",
"LocalUnused": "disabled",
"FunctionUnused": "fatal",
"ImportUnused": "disabled",
"SameLineStatement": "fatal",
"ImplicitReturn": "disabled",
"UnknownGlobal": "fatal",
"GlobalUsedAsLocal": "fatal"
}
}
@@ -0,0 +1,56 @@
--[[
A centralized hub for basic metrics reporting.
This class is designed to provide a baseline exposure to the reporters.
Context specific Analytics.lua objects should be created in sub-projects,
to report specific actions like chat interactions, or game page interactions.
Analytics.lua and the reporters here in Common should serve to cover the
most common interactions.
]]
local AnalyticsService = game:GetService("RbxAnalyticsService")
local Reporters = script.Parent.AnalyticsReporters
local DiagReporter = require(Reporters.Diag)
local EventStreamReporter = require(Reporters.EventStream)
local GoogleAnalyticsReporter = require(Reporters.GoogleAnalytics)
local InfluxDbReporter = require(Reporters.Influx)
local Analytics = {}
Analytics.__index = Analytics
-- reportingService : (Service, optional) an object that exposes the same functions as AnalyticsService
function Analytics.new(reportingService)
if not reportingService then
reportingService = AnalyticsService
end
-- All public reporting functions are exposed by the objects defined in the properties
local self = {}
self.Diag = DiagReporter.new(reportingService)
self.EventStream = EventStreamReporter.new(reportingService)
self.GoogleAnalytics = GoogleAnalyticsReporter.new(reportingService)
self.InfluxDb = InfluxDbReporter.new(reportingService)
setmetatable(self, Analytics)
return self
end
function Analytics.mock()
-- create a reporting service that does not fire any requests out to the world
local fakeReportingService = {}
function fakeReportingService.ReportCounter() end
function fakeReportingService.ReportInfluxSeries() end
function fakeReportingService.ReportStats() end
function fakeReportingService.SetRBXEvent() end
function fakeReportingService.SetRBXEventStream() end
function fakeReportingService.TrackEvent() end
function fakeReportingService.UpdateHeartbeatObject() end
return Analytics.new(fakeReportingService)
end
return Analytics
@@ -0,0 +1,69 @@
return function()
local Analytics = require(script.Parent.Analytics)
describe("new()", function()
it("should properly construct a new object", function()
local na = Analytics.new()
expect(na).to.be.ok()
end)
it("should accept a custom reporting service", function()
local fakeService = {}
local na = Analytics.new(fakeService)
expect(na).to.be.ok()
end)
it("should have a reporter specifically for Diag", function()
local na = Analytics.new()
expect(na.Diag).to.be.ok()
end)
it("should have a reporter specifically for RBXEventStream", function()
local na = Analytics.new()
expect(na.EventStream).to.be.ok()
end)
it("should have a reporter specifically for Google Analytics", function()
local na = Analytics.new()
expect(na.GoogleAnalytics).to.be.ok()
end)
it("should have a reporter specifically for Influx", function()
local na = Analytics.new()
expect(na.InfluxDb).to.be.ok()
end)
end)
describe("mock()", function()
it("should properly construct a new object", function()
local ma = Analytics.mock()
expect(ma).to.be.ok()
expect(ma.Diag).to.be.ok()
expect(ma.EventStream).to.be.ok()
expect(ma.GoogleAnalytics).to.be.ok()
expect(ma.InfluxDb).to.be.ok()
end)
it("should succeed for all function calls in Diag", function()
local ma = Analytics.mock()
ma.Diag:reportCounter("fakeCounter", 1)
ma.Diag:reportStats("fakeCategory", 1)
end)
it("should succeed for all function call in EventStream", function()
local ma = Analytics.mock()
ma.EventStream:setRBXEvent("fakeContext", "fakeEventName")
ma.EventStream:setRBXEventStream("fakeContext", "fakeEventName")
end)
it("should succeed for all function call in GoogleAnalytics", function()
local ma = Analytics.mock()
ma.GoogleAnalytics:trackEvent("fakeCategory", "fakeAction", "fakeLabel")
end)
it("should succeed for all function call in Influx", function()
local ma = Analytics.mock()
ma.InfluxDb:reportSeries("fakeSeries", {}, 1)
end)
end)
end
@@ -0,0 +1,64 @@
--[[
Specialized analytics reporter for ephemeral counters.
Useful for tracking at-a-glance health of a feature.
]]
local UserInputService = game:GetService("UserInputService")
local Diag = {}
Diag.__index = Diag
-- reportingService - (object) any object that defines the same functions for Diag as AnalyticsService
function Diag.new(reportingService)
local rsType = type(reportingService)
assert(rsType == "table" or rsType == "userdata", "Unexpected value for reportingService")
local self = {
_reporter = reportingService,
_isEnabled = true,
}
setmetatable(self, Diag)
return self
end
-- isEnabled : (boolean)
function Diag:setEnabled(isEnabled)
assert(type(isEnabled) == "boolean", "Expected isEnabled to be a boolean")
self._isEnabled = isEnabled
end
-- counterName : (string) the name of the ephemeral counter to increment
-- amount : (int) the value to increment the counter by
function Diag:reportCounter(counterName, amount)
assert(type(counterName) == "string", "Expected counterName to be a string")
assert(type(amount) == "number", "Expected amount to be a number")
assert(self._isEnabled, "This reporting service is disabled")
-- use special naming convention for Xbox counters
-- the call to GetPlatform is wrapped in a pcall() because the Testing Service
-- executes the scripts in the wrong authorization level
local platformID = Enum.Platform.None
pcall(function()
platformID = UserInputService:GetPlatform()
end)
if platformID == Enum.Platform.XBoxOne then
counterName = "Xbox-" .. tostring(counterName)
end
-- the AnalyticsService should automatically handle batch reporting
self._reporter:ReportCounter(counterName, amount)
end
-- category : (string) the name of the statistics buffer which to append the data
-- value : (number) any type of numeric value
function Diag:reportStats(category, value)
assert(type(category) == "string", "Expected category to be a string")
assert(type(value) == "number", "Expected value to be a number")
assert(self._isEnabled, "This reporting service is disabled")
self._reporter:ReportStats(category, value)
end
return Diag
@@ -0,0 +1,176 @@
return function()
local Diag = require(script.Parent.Diag)
local testCounterName = "testCounter"
local testCounterAmount = 1
local testCategoryName = "testCategory"
local testCategoryValue = 98
local badTestCounterName = 5
local badTestCounterAmount = "hello"
local badTestCategoryName = {}
local badTestCategoryValue = {}
local DebugReportingService = {}
function DebugReportingService:ReportCounter(counterName, amount)
assert(counterName == testCounterName, "Unexpected value for counterName: " .. counterName)
assert(amount == testCounterAmount, "Unexpected value for amount: " .. amount)
end
function DebugReportingService:ReportStats(categoryName, value)
assert(categoryName == testCategoryName, "Unexpected value for category: " .. categoryName)
if value then
assert(value == testCategoryValue, "Unexpected value for value: " .. value)
end
end
describe("new()", function()
it("should construct with a Reporting Service", function()
local diag = Diag.new(DebugReportingService)
expect(diag).to.be.ok()
end)
it("should throw an error to be constructed without a Reporting Service", function()
expect(function()
Diag.new(nil)
end).to.throw()
end)
end)
describe("setEnabled()", function()
it("should succeed with valid input", function()
local diag = Diag.new(DebugReportingService)
diag:setEnabled(false)
diag:setEnabled(true)
end)
it("should disable the reporter", function()
local diag = Diag.new(DebugReportingService)
diag:setEnabled(false)
expect(function()
diag:reportCounter(testCounterName, testCounterAmount)
end).to.throw()
end)
end)
describe("reportCounter()", function()
it("should work when appropriately enabled / disabled", function()
local diag = Diag.new(DebugReportingService)
expect(function()
diag:setEnabled(false)
diag:reportCounter(testCounterName, testCounterAmount)
end).to.throw()
diag:setEnabled(true)
diag:reportCounter(testCounterName, testCounterAmount)
end)
it("should succeed with valid input", function()
local diag = Diag.new(DebugReportingService)
diag:reportCounter(testCounterName, testCounterAmount)
end)
it("should throw an error with invalid input for the counter name", function()
local diag = Diag.new(DebugReportingService)
expect(function()
diag:reportCounter(badTestCounterName, testCounterAmount)
end).to.throw()
end)
it("should throw an error with invalid input for the amount", function()
local diag = Diag.new(DebugReportingService)
expect(function()
diag:reportCounter(testCounterName, badTestCounterAmount)
end).to.throw()
end)
it("should throw an error with completely invalid input", function()
local diag = Diag.new(DebugReportingService)
expect(function()
diag:reportCounter(badTestCounterName, badTestCounterAmount)
end).to.throw()
end)
it("should throw an error if it is missing a counter name", function()
local diag = Diag.new(DebugReportingService)
expect(function()
diag:reportCounter(nil, testCounterAmount)
end).to.throw()
end)
it("should throw an error if it is missing an amount", function()
local diag = Diag.new(DebugReportingService)
expect(function()
diag:reportCounter(testCounterName, nil)
end).to.throw()
end)
it("should throw an error if it is missing any input", function()
local diag = Diag.new(DebugReportingService)
expect(function()
diag:reportCounter(nil, nil)
end).to.throw()
end)
end)
describe("reportStats()", function()
it("should work when appropriately enabled / disabled", function()
local diag = Diag.new(DebugReportingService)
expect(function()
diag:setEnabled(false)
diag:reportStats(testCategoryName, testCategoryValue)
end).to.throw()
diag:setEnabled(true)
diag:reportStats(testCategoryName, testCategoryValue)
end)
it("should succeed with valid input", function()
local diag = Diag.new(DebugReportingService)
diag:reportStats(testCategoryName, testCategoryValue)
end)
it("should throw an error with invalid input for the category name", function()
local diag = Diag.new(DebugReportingService)
expect(function()
diag:reportStats(badTestCategoryName, testCategoryValue)
end).to.throw()
end)
it("should throw an error with invalid input for the value", function()
local diag = Diag.new(DebugReportingService)
expect(function()
diag:reportStats(testCategoryName, badTestCategoryValue)
end).to.throw()
end)
it("should throw an error with completely invalid input", function()
local diag = Diag.new(DebugReportingService)
expect(function()
diag:reportStats(badTestCategoryName, badTestCategoryValue)
end).to.throw()
end)
it("should throw an error if it is missing a category name", function()
local diag = Diag.new(DebugReportingService)
expect(function()
diag:reportStats(nil, testCategoryValue)
end).to.throw()
end)
it("should throw an error if it is missing a value", function()
local diag = Diag.new(DebugReportingService)
expect(function()
diag:reportStats(testCategoryName, nil)
end).to.throw()
end)
it("should throw an error if it is missing any input", function()
local diag = Diag.new(DebugReportingService)
expect(function()
diag:reportStats(nil, nil)
end).to.throw()
end)
end)
end
@@ -0,0 +1,154 @@
--[[
Specialized reporter for RBX Event Ingest data.
Useful for tracking explicit user interactions with screens and guis.
]]
local UserInputService = game:GetService("UserInputService")
local function getPlatformTarget()
local platformTarget = "unknownLua"
local platformEnum = Enum.Platform.None
-- the call to GetPlatform is wrapped in a pcall() because the Testing Service
-- executes the scripts in the wrong authorization level
pcall(function()
platformEnum = UserInputService:GetPlatform()
end)
-- bucket the platform based on consumer platform
local isDesktopClient = (platformEnum == Enum.Platform.Windows) or (platformEnum == Enum.Platform.OSX)
local isMobileClient = (platformEnum == Enum.Platform.IOS) or (platformEnum == Enum.Platform.Android)
isMobileClient = isMobileClient or (platformEnum == Enum.Platform.UWP)
local isConsole = (platformEnum == Enum.Platform.XBox360) or (platformEnum == Enum.Platform.XBoxOne)
isConsole = isConsole or (platformEnum == Enum.Platform.PS3) or (platformEnum == Enum.Platform.PS4)
isConsole = isConsole or (platformEnum == Enum.Platform.WiiU)
-- assign a target based on the form factor
if isDesktopClient then
platformTarget = "client"
elseif isMobileClient then
platformTarget = "mobile"
elseif isConsole then
platformTarget = "console"
else
-- if we don't have a name for the form factor, report it here so that we can eventually track it down
platformTarget = platformTarget .. tostring(platformEnum)
end
return platformTarget
end
local EventStream = {}
EventStream.__index = EventStream
-- reportingService - (object) any object that defines the same functions for Event Stream as AnalyticsService
function EventStream.new(reportingService)
local rsType = type(reportingService)
assert(rsType == "table" or rsType == "userdata", "Unexpected value for reportingService")
local self = {
_reporter = reportingService,
_isEnabled = true,
}
setmetatable(self, EventStream)
return self
end
-- isEnabled : (boolean)
function EventStream:setEnabled(isEnabled)
assert(type(isEnabled) == "boolean", "Expected isEnabled to be a boolean")
self._isEnabled = isEnabled
end
-- eventContext : (string) the location or context in which the event is occurring.
-- eventName : (string) the name corresponding to the type of event to be reported. "screenLoaded" for example.
-- additionalArgs : (optional, map<string, Value>) table for additional information to appear in the event stream.
function EventStream:setRBXEvent(eventContext, eventName, additionalArgs)
local target = getPlatformTarget()
additionalArgs = additionalArgs or {}
assert(type(eventContext) == "string", "Expected eventContext to be a string")
assert(type(eventName) == "string", "Expected eventName to be a string")
assert(type(additionalArgs) == "table", "Expected additionalArgs to be a table")
assert(self._isEnabled, "This reporting service is disabled")
-- This function fires reports to the server right away
self._reporter:SetRBXEvent(target, eventContext, eventName, additionalArgs)
end
-- eventContext : (string) the location or context in which the event is occurring.
-- eventName : (string) the name corresponding to the type of event to be reported. "screenLoaded" for example.
-- additionalArgs : (optional, map<string, Value>) map for extra keys to appear in the event stream.
function EventStream:setRBXEventStream(eventContext, eventName, additionalArgs)
local target = getPlatformTarget()
additionalArgs = additionalArgs or {}
assert(type(eventContext) == "string", "Expected eventContext to be a string")
assert(type(eventName) == "string", "Expected eventName to be a string")
assert(type(additionalArgs) == "table", "Expected additionalArgs to be a table")
assert(self._isEnabled, "This reporting service is disabled")
-- this function sends reports to the server in batches, not real-time
self._reporter:SetRBXEventStream(target, eventContext, eventName, additionalArgs)
end
function EventStream:releaseRBXEventStream()
assert(self._isEnabled, "This reporting service is disabled")
self._reporter:ReleaseRBXEventStream(getPlatformTarget())
end
-- eventContext : (string) the location or context in which the event is occurring.
-- eventName : (string) the name corresponding to the type of event to be reported. "screenLoaded" for example.
-- additionalArgs : (optional, map<string, Value>) map for extra keys to appear in the event stream.
function EventStream:sendEventDeferred(eventContext, eventName, additionalArgs)
local target = getPlatformTarget()
additionalArgs = additionalArgs or {}
assert(type(eventContext) == "string", "Expected eventContext to be a string")
assert(type(eventName) == "string", "Expected eventName to be a string")
assert(type(additionalArgs) == "table", "Expected additionalArgs to be a table")
assert(self._isEnabled, "This reporting service is disabled")
-- this function sends reports to the server in batches, not real-time
self._reporter:SendEventDeferred(target, eventContext, eventName, additionalArgs)
end
-- eventContext : (string) the location or context in which the event is occurring.
-- eventName : (string) the name corresponding to the type of event to be reported. "screenLoaded" for example.
-- additionalArgs : (optional, map<string, Value>) map for extra keys to appear in the event stream.
function EventStream:sendEventImmediately(eventContext, eventName, additionalArgs)
local target = getPlatformTarget()
additionalArgs = additionalArgs or {}
assert(type(eventContext) == "string", "Expected eventContext to be a string")
assert(type(eventName) == "string", "Expected eventName to be a string")
assert(type(additionalArgs) == "table", "Expected additionalArgs to be a table")
assert(self._isEnabled, "This reporting service is disabled")
-- this function sends reports to the server in batches, not real-time
self._reporter:SendEventImmediately(target, eventContext, eventName, additionalArgs)
end
-- additionalArgs : (optional, map<string, string>) table for extra keys to appear in the event stream.
function EventStream:updateHeartbeatObject(additionalArgs)
additionalArgs = additionalArgs or {}
assert(type(additionalArgs) == "table", "Expected additionalArgs to be a table")
assert(self._isEnabled, "This reporting service is disabled")
self._reporter:UpdateHeartbeatObject(additionalArgs)
end
return EventStream
@@ -0,0 +1,393 @@
return function()
local EventStream = require(script.Parent.EventStream)
local testArgs = {
testKey = "testValue"
}
local testContext = "testContext"
local testEvent = "testEventName"
local badTestArgs = "hello"
local badTestContext = {}
local badTestEvent = {}
local function isTableEqual(table1, table2)
if table1 == table2 then
return true
end
if type(table1) ~= "table" then
return false
end
if type(table2) ~= "table" then
return false
end
for key, _ in pairs(table1) do
if table1[key] ~= table2[key] then
return false
end
end
for key, _ in pairs(table2) do
if table2[key] ~= table1[key] then
return false
end
end
return true
end
local function createDebugReportingService()
local function validateInputs(eventTarget, eventContext, eventName, additionalArgs)
assert(eventTarget, "no value found for eventTarget")
assert(eventContext == testContext, "unexpected value for eventContext : " .. eventContext)
assert(eventName == testEvent, "unexpected value for eventName : " .. eventName)
if additionalArgs and not isTableEqual(additionalArgs, {}) then
assert(isTableEqual(additionalArgs, testArgs), "unexpected value for additionalArgs")
end
end
local DebugReportingService = {}
function DebugReportingService:SetRBXEvent(eventTarget, eventContext, eventName, additionalArgs)
validateInputs(eventTarget, eventContext, eventName, additionalArgs)
end
function DebugReportingService:SetRBXEventStream(eventTarget, eventContext, eventName, additionalArgs)
validateInputs(eventTarget, eventContext, eventName, additionalArgs)
end
function DebugReportingService:SendEventImmediately(eventTarget, eventContext, eventName, additionalArgs)
validateInputs(eventTarget, eventContext, eventName, additionalArgs)
end
function DebugReportingService:SendEventDeferred(eventTarget, eventContext, eventName, additionalArgs)
validateInputs(eventTarget, eventContext, eventName, additionalArgs)
end
function DebugReportingService:ReleaseRBXEventStream(eventTarget)
assert(eventTarget, "no value found for eventTarget")
end
function DebugReportingService:UpdateHeartbeatObject(additionalArgs)
if additionalArgs and not isTableEqual(additionalArgs, {}) then
assert(isTableEqual(additionalArgs, testArgs), "unexpected value for additionalArgs")
end
end
return DebugReportingService
end
describe("new()", function()
it("should construct with a Reporting Service", function()
local es = EventStream.new(createDebugReportingService())
expect(es).to.be.ok()
end)
it("should not allow construction without a Reporting Service", function()
expect(function()
EventStream.new(nil)
end).to.throw()
end)
end)
describe("setEnabled()", function()
it("should succeed with valid input", function()
local reporter = EventStream.new(createDebugReportingService())
reporter:setEnabled(false)
reporter:setEnabled(true)
end)
it("should disable the reporter", function()
local reporter = EventStream.new(createDebugReportingService())
reporter:setEnabled(false)
expect(function()
reporter:updateHeartbeatObject()
end).to.throw()
end)
end)
describe("setRBXEvent()", function()
it("should succeed with valid input", function()
local es = EventStream.new(createDebugReportingService())
es:setRBXEvent(testContext, testEvent, testArgs)
end)
it("should work when appropriately enabled / disabled", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:setEnabled(false)
es:setRBXEvent(testContext, testEvent, testArgs)
end).to.throw()
es:setEnabled(true)
es:setRBXEvent(testContext, testEvent, testArgs)
end)
it("should throw an error if it is missing a context", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:setRBXEvent(nil, testEvent, testArgs)
end).to.throw()
end)
it("should throw an error if it is missing an event name", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:setRBXEvent(testContext, nil, testArgs)
end).to.throw()
end)
it("should succeed even if there aren't any additional args", function()
local es = EventStream.new(createDebugReportingService())
es:setRBXEvent(testContext, testEvent, nil)
end)
it("should throw an error if it is given bad input for a context", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:setRBXEvent(badTestContext, testEvent, testArgs)
end).to.throw()
end)
it("should throw an error if it is given bad input for a event", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:setRBXEvent(testContext, badTestEvent, testArgs)
end).to.throw()
end)
it("should throw an error if it is given bad input for additionalArgs", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:setRBXEvent(testContext, testEvent, badTestArgs)
end).to.throw()
end)
end)
describe("setRBXEventStream()", function()
it("should succeed with valid input", function()
local es = EventStream.new(createDebugReportingService())
es:setRBXEventStream(testContext, testEvent, testArgs)
end)
it("should work when appropriately enabled / disabled", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:setEnabled(false)
es:setRBXEventStream(testContext, testEvent, testArgs)
end).to.throw()
es:setEnabled(true)
es:setRBXEventStream(testContext, testEvent, testArgs)
end)
it("should throw an error if it is missing a context", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:setRBXEventStream(nil, testEvent, testArgs)
end).to.throw()
end)
it("should throw an error if it is missing an event name", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:setRBXEventStream(testContext, nil, testArgs)
end).to.throw()
end)
it("should succeed even if there aren't any additional args", function()
local es = EventStream.new(createDebugReportingService())
es:setRBXEventStream(testContext, testEvent, nil)
end)
it("should throw an error if it is given bad input for a context", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:setRBXEventStream(badTestContext, testEvent, testArgs)
end).to.throw()
end)
it("should throw an error if it is given bad input for a event", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:setRBXEventStream(testContext, badTestEvent, testArgs)
end).to.throw()
end)
it("should throw an error if it is given bad input for additionalArgs", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:setRBXEventStream(testContext, testEvent, badTestArgs)
end).to.throw()
end)
end)
describe("sendEventImmediately()", function()
it("should succeed with valid input", function()
local es = EventStream.new(createDebugReportingService())
es:sendEventImmediately(testContext, testEvent, testArgs)
end)
it("should call without a fuss when enabled and throw when disabled", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:setEnabled(false)
es:sendEventImmediately(testContext, testEvent, testArgs)
end).to.throw()
es:setEnabled(true)
es:sendEventImmediately(testContext, testEvent, testArgs)
end)
it("should throw an error if it is missing a context", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:sendEventImmediately(nil, testEvent, testArgs)
end).to.throw()
end)
it("should throw an error if it is missing an event name", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:sendEventImmediately(testContext, nil, testArgs)
end).to.throw()
end)
it("should succeed even if there aren't any additional args", function()
local es = EventStream.new(createDebugReportingService())
es:sendEventImmediately(testContext, testEvent, nil)
end)
it("should throw an error if it is given bad input for a context", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:sendEventImmediately(badTestContext, testEvent, testArgs)
end).to.throw()
end)
it("should throw an error if it is given bad input for a event", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:sendEventImmediately(testContext, badTestEvent, testArgs)
end).to.throw()
end)
it("should throw an error if it is given bad input for additionalArgs", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:sendEventImmediately(testContext, testEvent, badTestArgs)
end).to.throw()
end)
end)
describe("sendEventDeferred()", function()
it("should succeed with valid input", function()
local es = EventStream.new(createDebugReportingService())
es:sendEventDeferred(testContext, testEvent, testArgs)
end)
it("should call without a fuss when enabled and throw when disabled", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:setEnabled(false)
es:sendEventDeferred(testContext, testEvent, testArgs)
end).to.throw()
es:setEnabled(true)
es:sendEventDeferred(testContext, testEvent, testArgs)
end)
it("should throw an error if it is missing a context", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:sendEventDeferred(nil, testEvent, testArgs)
end).to.throw()
end)
it("should throw an error if it is missing an event name", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:sendEventDeferred(testContext, nil, testArgs)
end).to.throw()
end)
it("should succeed even if there aren't any additional args", function()
local es = EventStream.new(createDebugReportingService())
es:sendEventDeferred(testContext, testEvent, nil)
end)
it("should throw an error if it is given bad input for a context", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:sendEventDeferred(badTestContext, testEvent, testArgs)
end).to.throw()
end)
it("should throw an error if it is given bad input for a event", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:sendEventDeferred(testContext, badTestEvent, testArgs)
end).to.throw()
end)
it("should throw an error if it is given bad input for additionalArgs", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:sendEventDeferred(testContext, testEvent, badTestArgs)
end).to.throw()
end)
end)
describe("releaseRBXEventStream()", function()
it("should succeed with valid input", function()
local es = EventStream.new(createDebugReportingService())
es:releaseRBXEventStream()
end)
it("should throw when disabled and succeed when enabled", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:setEnabled(false)
es:releaseRBXEventStream()
end).to.throw()
es:setEnabled(true)
es:releaseRBXEventStream()
end)
end)
describe("updateHeartbeatObject()", function()
it("should work when appropriately enabled / disabled", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:setEnabled(false)
es:updateHeartbeatObject(testArgs)
end).to.throw()
expect(function()
es:setEnabled(true)
es:updateHeartbeatObject(testArgs)
end).never.to.throw()
end)
it("should succeed with valid input", function()
local es = EventStream.new(createDebugReportingService())
es:updateHeartbeatObject(testArgs)
end)
it("should succeed even if there aren't any additional args", function()
local es = EventStream.new(createDebugReportingService())
es:updateHeartbeatObject(nil)
end)
it("should throw an error with invalid input", function()
local es = EventStream.new(createDebugReportingService())
expect(function()
es:updateHeartbeatObject(badTestArgs)
end).to.throw()
end)
end)
end
@@ -0,0 +1,51 @@
--[[
Specialized reporter for sending data to GA.
Useful for creating a breadcrumb trail of user interactions.
Events in GA are aggregated and organized in order by category, action, label.
]]
local GoogleAnalytics = {}
GoogleAnalytics.__index = GoogleAnalytics
-- reportingService : (table or userdata) any object that defines the same functions for GA as AnalyticsService
function GoogleAnalytics.new(reportingService)
local rsType = type(reportingService)
assert(rsType == "table" or rsType == "userdata", "Unexpected value for reportingService")
local self = {
_reporter = reportingService,
_isEnabled = true,
}
setmetatable(self, GoogleAnalytics)
return self
end
-- isEnabled : (boolean)
function GoogleAnalytics:setEnabled(isEnabled)
assert(type(isEnabled) == "boolean", "Expected isEnabled to be a boolean")
self._isEnabled = isEnabled
end
-- category : (string) the most generic category by which to organize data, ex) LuaApp, Errors, GameSettings, etc.
-- action : (string) a specific event to record, ex) ButtonPressed, GameExit
-- label : (string, optional) a detail to differentiate one action over another, ex) LoginButton, Exit Code 0
-- value : (integer, optional) the number of times this event has occurred
function GoogleAnalytics:trackEvent(category, action, label, value)
assert(type(category) == "string", "Expected category to be a string")
assert(type(action) == "string", "Expected action to be a string")
if label then
assert(type(label) == "string", "Expected label to be a string")
end
if value then
assert(type(value) == "number", "Expected value to be a number")
assert(value >= 0, "Expected value must not be a negative value")
end
assert(self._isEnabled, "This reporting service is disabled")
self._reporter:TrackEvent(category, action, label, value)
end
return GoogleAnalytics
@@ -0,0 +1,140 @@
return function()
local GoogleAnalytics = require(script.Parent.GoogleAnalytics)
local testCategory = "testCategory"
local testAction = "testAction"
local testLabel = "testLabel"
local testValue = 6
local badTestCategory = 13141
local badTestAction = {}
local badTestLabel = {}
local badTestValue = "heyo"
local DebugReportingService = {}
function DebugReportingService:TrackEvent(category, action, label, value)
if category ~= testCategory then
error("unexpected value for category: " .. category)
end
if action ~= testAction then
error("unexpected value for action: " .. action)
end
if label then
if label ~= testLabel then
error("unexpected value for label: " .. label)
end
end
if value then
if value ~= testValue then
error("unexpected value for value: " .. value)
end
end
end
describe("new()", function()
it("should construct with a Reporting Service and Logging Service", function()
local ga = GoogleAnalytics.new(DebugReportingService)
expect(ga).to.be.ok()
end)
it("should fail to be constructed without a Reporting Service", function()
expect(function()
GoogleAnalytics.new(nil)
end).to.throw()
end)
end)
describe("setEnabled()", function()
it("should succeed with valid input", function()
local reporter = GoogleAnalytics.new(DebugReportingService)
reporter:setEnabled(false)
reporter:setEnabled(true)
end)
it("should disable the reporter", function()
local reporter = GoogleAnalytics.new(DebugReportingService)
reporter:setEnabled(false)
expect(function()
reporter:trackEvent(testCategory, testAction, testLabel, testValue)
end).to.throw()
end)
end)
describe("trackEvent()", function()
it("should work when appropriately enabled / disabled", function()
local ga = GoogleAnalytics.new(DebugReportingService)
expect(function()
ga:setEnabled(false)
ga:trackEvent(testCategory, testAction, testLabel)
end).to.throw()
ga:setEnabled(true)
ga:trackEvent(testCategory, testAction, testLabel)
end)
it("should succeed with valid input", function()
local ga = GoogleAnalytics.new(DebugReportingService)
ga:trackEvent(testCategory, testAction, testLabel, testValue)
end)
it("should throw an error if it is missing a category", function()
local ga = GoogleAnalytics.new(DebugReportingService)
expect(function()
ga:trackEvent(nil, testAction, testLabel, testValue)
end).to.throw()
end)
it("should throw an error if it is missing a testAction", function()
local ga = GoogleAnalytics.new(DebugReportingService)
expect(function()
ga:trackEvent(testCategory, nil, testLabel, testValue)
end).to.throw()
end)
it("should not throw an error if it is missing a label", function()
local ga = GoogleAnalytics.new(DebugReportingService)
ga:trackEvent(testCategory, testAction, nil, testValue)
end)
it("should not throw an error if it is missing a value", function()
local ga = GoogleAnalytics.new(DebugReportingService)
ga:trackEvent(testCategory, testAction, testLabel)
end)
it("should throw an error if it is given invalid input for category", function()
local ga = GoogleAnalytics.new(DebugReportingService)
expect(function()
ga:trackEvent(badTestCategory, testAction, testLabel, testValue)
end).to.throw()
end)
it("should throw an error if it is given invalid input for action", function()
local ga = GoogleAnalytics.new(DebugReportingService)
expect(function()
ga:trackEvent(testCategory, badTestAction, testLabel, testValue)
end).to.throw()
end)
it("should throw an error if it is given invalid input for label", function()
local ga = GoogleAnalytics.new(DebugReportingService)
expect(function()
ga:trackEvent(testCategory, testAction, badTestLabel, testValue)
end).to.throw()
end)
it("should throw an error if it is given invalid input for value", function()
local ga = GoogleAnalytics.new(DebugReportingService)
expect(function()
ga:trackEvent(testCategory, testAction, testLabel, badTestValue)
end).to.throw()
expect(function()
ga:trackEvent(testCategory, testAction, testLabel, -1)
end).to.throw()
end)
end)
end
@@ -0,0 +1,47 @@
--[[
Specialized reporter for sending data to InfluxDb.
Useful for very detailed information about specific errors.
Due to how Influx sends data, it is disallowed on XBox.
~Kyler Mulherin (9/12/2017)
]]
local Influx = {}
Influx.__index = Influx
-- reportingService - (object) any object that defines the same functions for Influx as AnalyticsService
function Influx.new(reportingService)
local rsType = type(reportingService)
assert(rsType == "table" or rsType == "userdata", "Unexpected value for reportingService")
local self = {
_reporter = reportingService,
_isEnabled = true,
}
setmetatable(self, Influx)
return self
end
-- isEnabled : (boolean)
function Influx:setEnabled(isEnabled)
assert(type(isEnabled) == "boolean", "Expected isEnabled to be a boolean")
self._isEnabled = isEnabled
end
-- seriesName : (string) the name of the series as it will appear in InfluxDb
-- additionalArgs : (map<string, string>) extra key/values to appear in each series
-- throttlingPercent : (int) the chance to actually report this series
function Influx:reportSeries(seriesName, additionalArgs, throttlingPercent)
additionalArgs = additionalArgs or {}
assert(type(seriesName) == "string", "Expected seriesName to be a string")
assert(type(additionalArgs) == "table", "Expected additionalArgs to be a table")
assert(type(throttlingPercent) == "number", "Expected throttlingPercent to be a number")
assert(throttlingPercent >= 0 and throttlingPercent <= 10000, "throttlingPercent must be between 0 - 10,000")
assert(self._isEnabled, "This reporting service is disabled")
self._reporter:ReportInfluxSeries(seriesName, additionalArgs, throttlingPercent)
end
return Influx
@@ -0,0 +1,165 @@
return function()
local Influx = require(script.Parent.Influx)
local testSeriesName = "testSeries"
local testArgs = {
testKey = "testValue"
}
local testThrottlingPercentage = 1000
local badTestSeriesName = 114
local badTestArgs = "someString"
local badThrottlingPercentage1 = -15
local badThrottlingPercentage2 = 150000
local badThrottlingPercentage3 = "a lot"
local function isTableEqual(table1, table2)
if table1 == table2 then
return true
end
if type(table2) ~= "table" then
return false
end
for key, _ in pairs(table1) do
if table1[key] ~= table2[key] then
return false
end
end
for key, _ in pairs(table2) do
if table2[key] ~= table1[key] then
return false
end
end
return true
end
local DebugReportingService = {}
function DebugReportingService:ReportInfluxSeries(seriesName, additionalArgs, throttlingPercentage)
if seriesName ~= seriesName then
error("Unexpected value for seriesName: " .. seriesName)
end
if throttlingPercentage ~= testThrottlingPercentage then
error("Unexpected value for throttlingPercentage: " .. throttlingPercentage)
end
if isTableEqual(additionalArgs, {}) == false then
if isTableEqual(additionalArgs, testArgs) == false then
error("Unexpected value for additionalArgs")
end
end
end
describe("new()", function()
it("should construct with a Reporting Service", function()
local influx = Influx.new(DebugReportingService)
expect(influx).to.be.ok()
end)
it("should fail to be constructed without a Reporting Service", function()
expect(function()
Influx.new(nil)
end).to.throw()
end)
end)
describe("setEnabled()", function()
it("should succeed with valid input", function()
local influx = Influx.new(DebugReportingService)
influx:setEnabled(false)
influx:setEnabled(true)
end)
it("should disable the reporter", function()
local influx = Influx.new(DebugReportingService)
influx:setEnabled(false)
expect(function()
influx:reportSeries(testSeriesName, testArgs, testThrottlingPercentage)
end).to.throw()
end)
end)
describe("reportSeries()", function()
it("should work when appropriately enabled / disabled", function()
local influx = Influx.new(DebugReportingService)
expect(function()
influx:setEnabled(false)
influx:reportSeries(testSeriesName, testArgs, testThrottlingPercentage)
end).to.throw()
influx:setEnabled(true)
influx:reportSeries(testSeriesName, testArgs, testThrottlingPercentage)
end)
it("should succeed with valid input", function()
local influx = Influx.new(DebugReportingService)
influx:reportSeries(testSeriesName, testArgs, testThrottlingPercentage)
end)
it("should succeed even if it is missing any additionalArgs", function()
local influx = Influx.new(DebugReportingService)
influx:reportSeries(testSeriesName, nil, testThrottlingPercentage)
end)
it("should throw an error with invalid input for the seriesName", function()
local influx = Influx.new(DebugReportingService)
expect(function()
influx:reportSeries(badTestSeriesName, testArgs, testThrottlingPercentage)
end).to.throw()
end)
it("should throw an error with invalid input for the throttlingPercentage - out of range - below zero", function()
expect(function()
local influx = Influx.new(DebugReportingService)
influx:reportSeries(testSeriesName, testArgs, badThrottlingPercentage1)
end).to.throw()
end)
it("should throw an error with invalid input for the throttlingPercentage - out of range - above cap", function()
expect(function()
local influx = Influx.new(DebugReportingService)
influx:reportSeries(testSeriesName, testArgs, badThrottlingPercentage2)
end).to.throw()
end)
it("should throw an error with invalid input for the throttlingPercentage - bad type", function()
expect(function()
local influx = Influx.new(DebugReportingService)
influx:reportSeries(testSeriesName, testArgs, badThrottlingPercentage3)
end).to.throw()
end)
it("should throw an error with completely invalid input", function()
local influx = Influx.new(DebugReportingService)
expect(function()
influx:reportSeries(badTestSeriesName, badTestArgs, badThrottlingPercentage1)
end).to.throw()
end)
it("should throw an error if it is missing a seriesName", function()
local influx = Influx.new(DebugReportingService)
expect(function()
influx:reportSeries(nil, testArgs, testThrottlingPercentage)
end).to.throw()
end)
it("should throw an error if it is missing a throttlingPercentage", function()
local influx = Influx.new(DebugReportingService)
expect(function()
influx:reportSeries(testSeriesName, testArgs, nil)
end).to.throw()
end)
it("should throw an error if it is missing any input", function()
local influx = Influx.new(DebugReportingService)
expect(function()
influx:reportSeries(nil, nil, nil)
end).to.throw()
end)
end)
end
@@ -0,0 +1,7 @@
{
"lint": {
"LocalUnused": "fatal",
"ImportUnused": "fatal",
"DeprecatedGlobal": "fatal"
}
}
@@ -0,0 +1,9 @@
{
"language": {
"mode": "nonstrict"
},
"lint": {
"LocalShadow": "fatal",
"ImplicitReturn": "fatal"
}
}
@@ -0,0 +1,8 @@
-----------------------------------------------------------------------------
--- ---
--- Under Migration to CorePackages ---
--- ---
--- Please put your changes in Analytics. ---
-----------------------------------------------------------------------------
local CorePackages = game:GetService("CorePackages")
return require(CorePackages.Analytics.AnalyticsReporters.Diag)
@@ -0,0 +1,8 @@
{
"language": {
"mode": "nonstrict"
},
"lint": {
"ImplicitReturn": "fatal"
}
}
@@ -0,0 +1,68 @@
--[[
A helper function to define a Rodux action creator with an associated name.
Normally when creating a Rodux action, you can just create a function:
return function(value)
return {
type = "MyAction",
value = value,
}
end
And then when you check for it in your reducer, you either use a constant,
or type out the string name:
if action.type == "MyAction" then
-- change some state
end
Typos here are a remarkably common bug. We also have the issue that there's
no link between reducers and the actions that they respond to!
`Action` (this helper) provides a utility that makes this a bit cleaner.
Instead, define your Rodux action like this:
return Action("MyAction", function(value)
return {
value = value,
}
end)
We no longer need to add the `type` field manually.
Additionally, the returned action creator now has a 'name' property that can
be checked by your reducer:
local MyAction = require(Reducers.MyAction)
...
if action.type == MyAction.name then
-- change some state!
end
Now we have a clear link between our reducers and the actions they use, and
if we ever typo a name, we'll get a warning in LuaCheck as well as an error
at runtime!
]]
return function(name, fn)
assert(type(name) == "string", "A name must be provided to create an Action")
assert(type(fn) == "function", "A function must be provided to create an Action")
return setmetatable({
name = name,
}, {
__call = function(self, ...)
local result = fn(...)
assert(type(result) == "table", "An action must return a table")
result.type = name
return result
end
})
end
@@ -0,0 +1,85 @@
return function()
local Action = require(script.Parent.Action)
it("should return a table", function()
local action = Action("foo", function()
return {}
end)
expect(action).to.be.a("table")
end)
it("should set the name of the action", function()
local action = Action("foo", function()
return {}
end)
expect(action.name).to.equal("foo")
end)
it("should be able to be called as a function", function()
local action = Action("foo", function()
return {}
end)
expect(action).never.to.throw()
end)
it("should return a table when called as a function", function()
local action = Action("foo", function()
return {}
end)
expect(action()).to.be.a("table")
end)
it("should set the type of the action", function()
local action = Action("foo", function()
return {}
end)
expect(action().type).to.equal("foo")
end)
it("should set values", function()
local action = Action("foo", function(value)
return {
value = value
}
end)
expect(action(100).value).to.equal(100)
end)
it("should throw when passed a function", function()
local action = Action("foo", function()
return function() end
end)
expect(action).to.throw()
end)
it("should throw with a invalid name", function()
expect(function()
Action(nil, function()
return {}
end)
end).to.throw()
expect(function()
Action(100, function()
return {}
end)
end).to.throw()
end)
it("should throw when passed a invalid function", function()
expect(function()
Action("foo", nil)
end).to.throw()
expect(function()
Action("foo", {})
end).to.throw()
end)
end
@@ -0,0 +1,19 @@
local Color = {}
function Color.RgbFromHex(hexColor)
assert(hexColor >= 0 and hexColor <= 0xffffff, "RgbFromHex: Out of range")
local b = hexColor % 256
hexColor = (hexColor - b) / 256
local g = hexColor % 256
hexColor = (hexColor - g) / 256
local r = hexColor
return r, g, b
end
function Color.Color3FromHex(hexColor)
return Color3.fromRGB(Color.RgbFromHex(hexColor))
end
return Color
@@ -0,0 +1,33 @@
return function()
local Color = require(script.Parent.Color)
describe("RgbFromHex", function()
it("should convert a hex color to rgb correctly", function()
local r, g, b = Color.RgbFromHex(0x232527)
expect(r).to.equal(35)
expect(g).to.equal(37)
expect(b).to.equal(39)
r, g, b = Color.RgbFromHex(0x0)
expect(r).to.equal(0)
expect(g).to.equal(0)
expect(b).to.equal(0)
r, g, b = Color.RgbFromHex(0xffffff)
expect(r).to.equal(255)
expect(g).to.equal(255)
expect(b).to.equal(255)
end)
it("should assert if given a hex color out of range", function()
expect(function()
Color.RgbFromHex(-1)
end).to.throw()
expect(function()
Color.RgbFromHex(0x1000000)
end).to.throw()
end)
end)
end
@@ -0,0 +1,131 @@
--[[
Provides an implementation of functional programming primitives.
]]
local Functional = {}
--[[
Create a copy of a list with only values for which `callback` returns true
]]
function Functional.Filter(list, callback)
local new = {}
for key = 1, #list do
local value = list[key]
if callback(value, key) then
table.insert(new, value)
end
end
return new
end
--[[
Create a copy of a list where each value is transformed by `callback`
]]
function Functional.Map(list, callback)
local new = {}
for key = 1, #list do
new[key] = callback(list[key], key)
end
return new
end
--[[
Identical to Map, except that the result will be reversed.
]]
function Functional.MapReverse(list, callback)
local new = {}
for key = #list, 1, -1 do
new[key] = callback(list[key], key)
end
return new
end
--[[
Create a copy of a list doing a combination filter and map.
If callback returns nil for any item, it is considered filtered from the
list. Any other value is considered the result of the 'map' operation.
]]
function Functional.FilterMap(list, callback)
local new = {}
for key = 1, #list do
local value = list[key]
local result = callback(value, key)
if result ~= nil then
table.insert(new, result)
end
end
return new
end
--[[
Performs a left-fold of the list with the given initial value and callback.
]]
function Functional.Fold(list, initial, callback)
local accum = initial
for key = 1, #list do
accum = callback(accum, list[key], key)
end
return accum
end
--[[
Performs a fold over the entries in the given dictionary.
]]
function Functional.FoldDictionary(dictionary, initial, callback)
local accum = initial
for key, value in pairs(dictionary) do
accum = callback(accum, key, value)
end
return accum
end
--[[
Returns a list that contains at most `count` values from the given list.
]]
function Functional.Take(list, count, startingIndex)
startingIndex = startingIndex or 1
local maxIndex = count + (startingIndex - 1)
if maxIndex > #list then
maxIndex = #list
end
local new = {}
for i = startingIndex, maxIndex do
local value = list[i]
local newIndex = i - (startingIndex - 1)
new[newIndex] = value
end
return new
end
--[[
If the list contains the sought-after element, return its index, or nil otherwise.
]]
function Functional.Find(list, value)
for index, element in ipairs(list) do
if element == value then
return index
end
end
return nil
end
return Functional
@@ -0,0 +1,218 @@
return function()
local Functional = require(script.Parent.Functional)
local function identity(...)
return ...
end
local function add(a, b)
return a + b
end
describe("Filter", function()
it("should copy lists correctly", function()
local listA = {1, 2, 3}
local listB = Functional.Filter(listA, function()
return true
end)
expect(listB).never.to.equal(listA)
for i = 1, #listB do
expect(listB[i]).to.equal(listA[i])
end
end)
it("should correctly use the filter predicate", function()
local listA = {1, 2, 3, 4, 5}
local listB = Functional.Filter(listA, function(value, key)
expect(value).to.equal(key)
return value % 2 == 0
end)
expect(listB[1]).to.equal(2)
expect(listB[2]).to.equal(4)
end)
end)
describe("Map", function()
it("should copy lists correctly using the identity function", function()
local listA = {1, 2, 3}
local listB = Functional.Map(listA, identity)
expect(listB).never.to.equal(listA)
for i = 1, #listB do
expect(listB[i]).to.equal(listA[i])
end
end)
it("should correctly use the map predicate", function()
local listA = {1, 2, 3}
local listB = Functional.Map(listA, function(value, key)
expect(value).to.equal(key)
return value * 2
end)
for i = 1, #listB do
expect(listB[i]).to.equal(listA[i] * 2)
end
end)
end)
describe("MapReverse", function()
it("should copy lists correctly using the identity function", function()
local listA = {1, 2, 3}
local listB = Functional.MapReverse(listA, identity)
expect(listB).never.to.equal(listA)
for i = 1, #listB do
expect(listB[i]).to.equal(listA[i])
end
end)
it("should correctly use the map predicate", function()
local listA = {1, 2, 3}
local listB = Functional.MapReverse(listA, function(value, key)
expect(value).to.equal(key)
return value * 2
end)
for i = 1, #listB do
expect(listB[i]).to.equal(listA[i] * 2)
end
end)
it("should iterate backwards", function()
local list = {1, 2, 3}
local nextKey = 3
Functional.MapReverse(list, function(value, key)
expect(value).to.equal(nextKey)
expect(key).to.equal(nextKey)
nextKey = nextKey - 1
end)
expect(nextKey).to.equal(0)
end)
end)
describe("FilterMap", function()
it("should copy truthy lists using the identity function", function()
local listA = {1, 2, 3}
local listB = Functional.FilterMap(listA, identity)
expect(listB).never.to.equal(listA)
for i = 1, #listB do
expect(listB[i]).to.equal(listA[i])
end
end)
it("should correctly use the filter-map predicate", function()
local listA = {1, 2, 3, 4, 5}
-- Create a list containing only the odd numbers, and double those numbers
local listB = Functional.FilterMap(listA, function(value, key)
expect(value).to.equal(key)
if value % 2 == 0 then
return nil
end
return value * 2
end)
expect(listB[1]).to.equal(2)
expect(listB[2]).to.equal(6)
expect(listB[3]).to.equal(10)
end)
end)
describe("Fold", function()
it("should left-fold lists", function()
local list = {1, 2, 3, 4, 5}
local sum = Functional.Fold(list, 0, add)
expect(sum).to.equal(15)
end)
end)
describe("Take", function()
it("should take values from a list", function()
local a = {1, 2, 3}
local b = Functional.Take(a, 2)
expect(#b).to.equal(2)
expect(b[1]).to.equal(1)
expect(b[2]).to.equal(2)
end)
it("should not take past the end of a list", function()
local a = {1, 2, 3}
local b = Functional.Take(a, 4)
expect(#b).to.equal(3)
expect(b[1]).to.equal(1)
expect(b[2]).to.equal(2)
expect(b[3]).to.equal(3)
end)
it("should copy all values when taking past the end of a list", function()
local a = {1, 2, 3}
local b = Functional.Take(a, 4)
expect(#b).to.equal(#a)
expect(a[1]).to.equal(b[1])
expect(a[2]).to.equal(b[2])
expect(a[3]).to.equal(b[3])
end)
it("should take values from a starting index when provided", function()
local a = {1, 2, 3, 4}
local b = Functional.Take(a, 2, 2)
expect(#b).to.equal(2)
expect(b[1]).to.equal(2)
expect(b[2]).to.equal(3)
end)
it("should not take past the end of a list when the starting index is provided", function()
local a = {1, 2, 3, 4}
local b = Functional.Take(a, 3, 3)
expect(#b).to.equal(2)
expect(b[1]).to.equal(3)
expect(b[2]).to.equal(4)
end)
end)
describe("Find", function()
it("should return index of matched item", function()
local a = {"foo", "bar", "garply"}
local b = Functional.Find(a, "bar")
expect(b).to.equal(2)
end)
it("should find the first example in the case of duplicates", function()
local a = {"foo", "bar", "garply", "bar"}
local b = Functional.Find(a, "bar")
expect(b).to.equal(2)
end)
it("should return nil if item is not found", function()
local a = {"foo", "bar", "garply"}
local b = Functional.Find(a, "fleebledegoop")
expect(b).to.equal(nil)
end)
end)
end
@@ -0,0 +1,141 @@
--[[
Provides functions for manipulating immutable data structures.
]]
local Immutable = {}
--[[
Merges dictionary-like tables together.
]]
function Immutable.JoinDictionaries(...)
local result = {}
for i = 1, select("#", ...) do
local dictionary = select(i, ...)
for key, value in pairs(dictionary) do
result[key] = value
end
end
return result
end
--[[
Joins any number of lists together into a new list
]]
function Immutable.JoinLists(...)
local new = {}
for listKey = 1, select("#", ...) do
local list = select(listKey, ...)
local len = #new
for itemKey = 1, #list do
new[len + itemKey] = list[itemKey]
end
end
return new
end
--[[
Creates a new copy of the dictionary and sets a value inside it.
]]
function Immutable.Set(dictionary, key, value)
local new = {}
for key, value in pairs(dictionary) do
new[key] = value
end
new[key] = value
return new
end
--[[
Creates a new copy of the list with the given elements appended to it.
]]
function Immutable.Append(list, ...)
local new = {}
local len = #list
for key = 1, len do
new[key] = list[key]
end
for i = 1, select("#", ...) do
new[len + i] = select(i, ...)
end
return new
end
--[[
Remove elements from a dictionary
]]
function Immutable.RemoveFromDictionary(dictionary, ...)
local result = {}
for key, value in pairs(dictionary) do
local found = false
for listKey = 1, select("#", ...) do
if key == select(listKey, ...) then
found = true
break
end
end
if not found then
result[key] = value
end
end
return result
end
--[[
Remove the given key from the list.
]]
function Immutable.RemoveFromList(list, removeIndex)
local new = {}
for i = 1, #list do
if i ~= removeIndex then
table.insert(new, list[i])
end
end
return new
end
--[[
Remove the range from the list starting from the index.
]]
function Immutable.RemoveRangeFromList(list, index, count)
local new = {}
for i = 1, #list do
if i < index or i >= index + count then
table.insert(new, list[i])
end
end
return new
end
--[[
Creates a new list that has no occurrences of the given value.
]]
function Immutable.RemoveValueFromList(list, removeValue)
local new = {}
for i = 1, #list do
if list[i] ~= removeValue then
table.insert(new, list[i])
end
end
return new
end
return Immutable
@@ -0,0 +1,284 @@
return function()
local Immutable = require(script.Parent.Immutable)
describe("JoinDictionaries", function()
it("should preserve immutability", function()
local a = {}
local b = {}
local c = Immutable.JoinDictionaries(a, b)
expect(c).never.to.equal(a)
expect(c).never.to.equal(b)
end)
it("should treat list-like values like dictionary values", function()
local a = {
[1] = 1,
[2] = 2,
[3] = 3
}
local b = {
[1] = 11,
[2] = 22
}
local c = Immutable.JoinDictionaries(a, b)
expect(c[1]).to.equal(b[1])
expect(c[2]).to.equal(b[2])
expect(c[3]).to.equal(a[3])
end)
it("should merge dictionary values correctly", function()
local a = {
hello = "world",
foo = "bar"
}
local b = {
foo = "baz",
tux = "penguin"
}
local c = Immutable.JoinDictionaries(a, b)
expect(c.hello).to.equal(a.hello)
expect(c.foo).to.equal(b.foo)
expect(c.tux).to.equal(b.tux)
end)
it("should merge multiple dictionaries", function()
local a = {
foo = "yes"
}
local b = {
bar = "yup"
}
local c = {
baz = "sure"
}
local d = Immutable.JoinDictionaries(a, b, c)
expect(d.foo).to.equal(a.foo)
expect(d.bar).to.equal(b.bar)
expect(d.baz).to.equal(c.baz)
end)
end)
describe("JoinLists", function()
it("should preserve immutability", function()
local a = {}
local b = {}
local c = Immutable.JoinLists(a, b)
expect(c).never.to.equal(a)
expect(c).never.to.equal(b)
end)
it("should treat list-like values correctly", function()
local a = {1, 2, 3}
local b = {4, 5, 6}
local c = Immutable.JoinLists(a, b)
expect(#c).to.equal(6)
for i = 1, #c do
expect(c[i]).to.equal(i)
end
end)
it("should merge multiple lists", function()
local a = {1, 2}
local b = {3, 4}
local c = {5, 6}
local d = Immutable.JoinLists(a, b, c)
expect(#d).to.equal(6)
for i = 1, #d do
expect(d[i]).to.equal(i)
end
end)
end)
describe("Set", function()
it("should preserve immutability", function()
local a = {}
local b = Immutable.Set(a, "foo", "bar")
expect(b).never.to.equal(a)
end)
it("should treat numeric keys normally", function()
local a = {1, 2, 3}
local b = Immutable.Set(a, 2, 4)
expect(b[1]).to.equal(1)
expect(b[2]).to.equal(4)
expect(b[3]).to.equal(3)
end)
it("should overwrite dictionary-like keys", function()
local a = {
foo = "bar",
baz = "qux"
}
local b = Immutable.Set(a, "foo", "hello there")
expect(b.foo).to.equal("hello there")
expect(b.baz).to.equal(a.baz)
end)
end)
describe("Append", function()
it("should preserve immutability", function()
local a = {}
local b = Immutable.Append(a, "another happy landing")
expect(b).never.to.equal(a)
end)
it("should append values", function()
local a = {1, 2, 3}
local b = Immutable.Append(a, 4, 5)
expect(#b).to.equal(5)
for i = 1, #b do
expect(b[i]).to.equal(i)
end
end)
end)
describe("RemoveFromDictionary", function()
it("should preserve immutability", function()
local a = { foo = "bar" }
local b = Immutable.RemoveFromDictionary(a, "foo")
expect(b).to.never.equal(a)
end)
it("should remove fields from the dictionary", function()
local a = {
foo = "bar",
baz = "qux",
boof = "garply",
}
local b = Immutable.RemoveFromDictionary(a, "foo", "boof")
expect(b.foo).to.never.be.ok()
expect(b.baz).to.equal("qux")
expect(b.boof).to.never.be.ok()
end)
end)
describe("RemoveFromList", function()
it("should preserve immutability", function()
local a = {1, 2, 3}
local b = Immutable.RemoveFromList(a, 2)
expect(b).never.to.equal(a)
end)
it("should remove elements from the list", function()
local a = {1, 2, 3}
local b = Immutable.RemoveFromList(a, 2)
expect(b[1]).to.equal(1)
expect(b[2]).to.equal(3)
expect(b[3]).never.to.be.ok()
end)
end)
describe("RemoveRangeFromList", function()
it("should preserve immutability", function()
local a = {1, 2, 3}
local b = Immutable.RemoveRangeFromList(a, 2, 1)
expect(b).never.to.equal(a)
end)
it("should remove elements properly from the list 1", function()
local a = {1, 2, 3}
local b = Immutable.RemoveRangeFromList(a, 2, 1)
expect(b[1]).to.equal(1)
expect(b[2]).to.equal(3)
expect(b[3]).never.to.be.ok()
end)
it("should remove elements properly from the list 2", function()
local a = {1, 2, 3, 4, 5, 6}
local b = Immutable.RemoveRangeFromList(a, 1, 4)
expect(b[1]).to.equal(5)
expect(b[2]).to.equal(6)
expect(b[3]).never.to.be.ok()
end)
it("should remove elements properly from the list 3", function()
local a = {1, 2, 3, 4, 5, 6}
local b = Immutable.RemoveRangeFromList(a, 2, 4)
expect(b[1]).to.equal(1)
expect(b[2]).to.equal(6)
expect(b[3]).never.to.be.ok()
end)
it("should remove elements properly from the list 4", function()
local a = {1, 2, 3, 4, 5, 6, 7}
local b = Immutable.RemoveRangeFromList(a, 4, 4)
expect(b[1]).to.equal(1)
expect(b[2]).to.equal(2)
expect(b[3]).to.equal(3)
expect(b[4]).never.to.be.ok()
end)
it("should not remove any elements when count is 0 or less", function()
local a = {1, 2, 3}
local b = Immutable.RemoveRangeFromList(a, 2, 0)
expect(b[1]).to.equal(1)
expect(b[2]).to.equal(2)
expect(b[3]).to.equal(3)
local c = Immutable.RemoveRangeFromList(a, 2, -1)
expect(c[1]).to.equal(1)
expect(c[2]).to.equal(2)
expect(c[3]).to.equal(3)
end)
end)
describe("RemoveValueFromList", function()
it("should preserve immutability", function()
local a = {1, 1, 1}
local b = Immutable.RemoveValueFromList(a, 1)
expect(b).never.to.equal(a)
end)
it("should remove all elements from the list", function()
local a = {1, 2, 2, 3}
local b = Immutable.RemoveValueFromList(a, 2)
expect(b[1]).to.equal(1)
expect(b[2]).to.equal(3)
expect(b[3]).never.to.be.ok()
end)
end)
end
@@ -0,0 +1,75 @@
--[[
A limited, simple implementation of a Signal.
Handlers are fired in order, and (dis)connections are properly handled when
executing an event.
Signal uses Immutable to avoid invalidating the 'Fire' loop iteration.
]]
local Immutable = require(script.Parent.Immutable)
local Signal = {}
Signal.__index = Signal
function Signal.new()
local self = {
_listeners = {}
}
setmetatable(self, Signal)
return self
end
function Signal:connect(callback)
local listener = {
callback = callback,
isConnected = true,
}
self._listeners = Immutable.Append(self._listeners, listener)
local function disconnect()
listener.isConnected = false
self._listeners = Immutable.RemoveValueFromList(self._listeners, listener)
end
return {
Disconnect = function()
warn(string.format(
"Connection:Disconnect() has been deprecated, use Connection:disconnect()\n%s]",
debug.traceback()
))
disconnect()
end,
disconnect = disconnect,
}
end
function Signal:fire(...)
for _, listener in ipairs(self._listeners) do
if listener.isConnected then
listener.callback(...)
end
end
end
function Signal:Connect(...)
warn(string.format(
"Signal:Connect() has been deprecated, use Signal:connect()\n%s]",
debug.traceback()
))
return self:connect(...)
end
function Signal:Fire(...)
warn(string.format(
"Signal:Fire() has been deprecated, use Signal:fire()\n%s]",
debug.traceback()
))
self:fire(...)
end
return Signal
@@ -0,0 +1,114 @@
return function()
local Signal = require(script.Parent.Signal)
it("should construct from nothing", function()
local signal = Signal.new()
expect(signal).to.be.ok()
end)
it("should fire connected callbacks", function()
local callCount = 0
local value1 = "Hello World"
local value2 = 7
local callback = function(arg1, arg2)
expect(arg1).to.equal(value1)
expect(arg2).to.equal(value2)
callCount = callCount + 1
end
local signal = Signal.new()
local connection = signal:connect(callback)
signal:fire(value1, value2)
expect(callCount).to.equal(1)
connection:disconnect()
signal:fire(value1, value2)
expect(callCount).to.equal(1)
end)
it("should disconnect handlers", function()
local callback = function()
error("Callback was called after disconnect!")
end
local signal = Signal.new()
local connection = signal:connect(callback)
connection:disconnect()
signal:fire()
end)
it("should fire handlers in order", function()
local signal = Signal.new()
local x = 0
local y = 0
local callback1 = function()
expect(x).to.equal(0)
expect(y).to.equal(0)
x = x + 1
end
local callback2 = function()
expect(x).to.equal(1)
expect(y).to.equal(0)
y = y + 1
end
signal:connect(callback1)
signal:connect(callback2)
signal:fire()
expect(x).to.equal(1)
expect(y).to.equal(1)
end)
it("should continue firing despite mid-event disconnection", function()
local signal = Signal.new()
local countA = 0
local countB = 0
local connectionA
connectionA = signal:connect(function()
connectionA:disconnect()
countA = countA + 1
end)
signal:connect(function()
countB = countB + 1
end)
signal:fire()
expect(countA).to.equal(1)
expect(countB).to.equal(1)
end)
it("should skip listeners that were disconnected during event evaluation", function()
local signal = Signal.new()
local countA = 0
local countB = 0
local connectionB
signal:connect(function()
countA = countA + 1
connectionB:disconnect()
end)
connectionB = signal:connect(function()
countB = countB + 1
end)
signal:fire()
expect(countA).to.equal(1)
expect(countB).to.equal(0)
end)
end
@@ -0,0 +1,113 @@
local EngineFeatureTextBoundsRoundUp = game:GetEngineFeature("TextBoundsRoundUp")
local TextService = game:GetService("TextService")
local Text = {}
-- FYI: Any number greater than 2^30 will make TextService:GetTextSize give invalid results
local MAX_BOUND = 10000
-- Remove with EngineFeatureTextBoundsRoundUp
Text._TEMP_PATCHED_PADDING = Vector2.new(0, 0)
if not EngineFeatureTextBoundsRoundUp then
Text._TEMP_PATCHED_PADDING = Vector2.new(2, 2)
end
-- Wrapper function for GetTextSize
function Text.GetTextBounds(text, font, fontSize, bounds)
return TextService:GetTextSize(text, fontSize, font, bounds) + Text._TEMP_PATCHED_PADDING
end
function Text.GetTextWidth(text, font, fontSize)
return Text.GetTextBounds(text, font, fontSize, Vector2.new(MAX_BOUND, MAX_BOUND)).X
end
function Text.GetTextHeight(text, font, fontSize, widthCap)
return Text.GetTextBounds(text, font, fontSize, Vector2.new(widthCap, MAX_BOUND)).Y
end
-- TODO(CLIPLAYEREX-391): Kill these truncate functions once we have official support for text truncation
function Text.Truncate(text, font, fontSize, widthInPixels, overflowMarker)
overflowMarker = overflowMarker or ""
if Text.GetTextWidth(text, font, fontSize) > widthInPixels then
-- A binary search may be more efficient
local lastText = ""
for _, stopIndex in utf8.graphemes(text) do
local newText = string.sub(text, 1, stopIndex) .. overflowMarker
if Text.GetTextWidth(newText, font, fontSize) > widthInPixels then
return lastText
end
lastText = newText
end
else -- No truncation needed
return text
end
return ""
end
function Text.TruncateTextLabel(textLabel, overflowMarker)
textLabel.Text = Text.Truncate(textLabel.Text, textLabel.Font,
textLabel.TextSize, textLabel.AbsoluteSize.X, overflowMarker)
end
-- Remove whitespace from the beginning and end of the string
function Text.Trim(str)
if type(str) ~= "string" then
error(string.format("Text.Trim called on non-string type %s.", type(str)), 2)
end
return (str:gsub("^%s*(.-)%s*$", "%1"))
end
-- Remove whitespace from the end of the string
function Text.RightTrim(str)
if type(str) ~= "string" then
error(string.format("Text.RightTrim called on non-string type %s.", type(str)), 2)
end
return (str:gsub("%s+$", ""))
end
-- Remove whitespace from the beginning of the string
function Text.LeftTrim(str)
if type(str) ~= "string" then
error(string.format("Text.LeftTrim called on non-string type %s.", type(str)), 2)
end
return (str:gsub("^%s+", ""))
end
-- Replace multiple whitespace with one; remove leading and trailing whitespace
function Text.SpaceNormalize(str)
if type(str) ~= "string" then
error(string.format("Text.SpaceNormalize called on non-string type %s.", type(str)), 2)
end
return (str:gsub("%s+", " "):gsub("^%s+" , ""):gsub("%s+$" , ""))
end
-- Splits a string by the provided pattern into a table. The pattern is interpreted as plain text.
function Text.Split(str, pattern)
if type(str) ~= "string" then
error(string.format("Text.Split called on non-string type %s.", type(str)), 2)
elseif type(pattern) ~= "string" then
error(string.format("Text.Split called with a pattern that is non-string type %s.", type(pattern)), 2)
elseif pattern == "" then
error("Text.Split called with an empty pattern.", 2)
end
local result = {}
local currentPosition = 1
while true do
local patternStart, patternEnd = string.find(str, pattern, currentPosition, true)
if not patternStart or not patternEnd then break end
table.insert(result, string.sub(str, currentPosition, patternStart - 1))
currentPosition = patternEnd + 1
end
table.insert(result, string.sub(str, currentPosition, string.len(str)))
return result
end
return Text
@@ -0,0 +1,410 @@
return function()
local Text = require(script.Parent.Text)
describe("GetTextBounds", function()
it("should return a bounds of padding width and font-size height when the string is empty", function()
local bounds = Text.GetTextBounds("", Enum.Font.SourceSans, 18, Vector2.new(1000, 1000))
expect(bounds.X).to.equal(Text._TEMP_PATCHED_PADDING.x)
expect(bounds.Y).to.equal(18 + Text._TEMP_PATCHED_PADDING.y)
end)
it("should return the height and width of a string as one line with large bounds", function()
local bounds = Text.GetTextBounds("One Two Three", Enum.Font.SourceSans, 18, Vector2.new(1000, 1000))
expect(bounds.Y).to.equal(18 + Text._TEMP_PATCHED_PADDING.y)
end)
it("should return the height of the string as multiple lines with short bounds", function()
local bounds = Text.GetTextBounds("One Two Three Four", Enum.Font.SourceSans, 18, Vector2.new(32, 1000))
expect(bounds.Y > 18).to.equal(true)
end)
end)
describe("GetTextHeight", function()
it("should return height equal to font size when string is empty", function()
local height = Text.GetTextHeight("", Enum.Font.SourceSans, 18, 0)
expect(height).to.equal(18 + Text._TEMP_PATCHED_PADDING.y)
end)
end)
describe("GetTextWidth", function()
it("should return width equal to 1 when string is empty", function()
local width = Text.GetTextWidth("", Enum.Font.SourceSans, 18, 18)
expect(width).to.equal(Text._TEMP_PATCHED_PADDING.x)
end)
end)
describe("Truncate", function()
it("should return empty string", function()
local emptyQuery = Text.Truncate("", Enum.Font.SourceSans, 18, 0, "...")
expect(emptyQuery).to.be.a("string")
expect(emptyQuery).to.equal("")
end)
it("should return empty string for not empty box", function()
local emptyQuery = Text.Truncate("", Enum.Font.SourceSans, 18, 50, "...")
expect(emptyQuery).to.be.a("string")
expect(emptyQuery).to.equal("")
end)
it("should truncate with ...", function()
local reallyLongQuery = Text.Truncate(
"One Two Three Four Five Six Seven Eight Nine Ten Eleven Twelve", Enum.Font.SourceSans, 18, 100, "...")
expect(reallyLongQuery).to.equal("One Two Thre...")
end)
it("should truncate without a ...", function()
local reallyLongQueryNoOverflowMarker = Text.Truncate(
"One Two Three Four Five Six Seven Eight Nine Ten Eleven Twelve", Enum.Font.SourceSans, 18, 100)
expect(reallyLongQueryNoOverflowMarker).to.equal("One Two Three ")
end)
it("should not truncate", function()
local shouldFitQuery = Text.Truncate("One Two", Enum.Font.SourceSans, 18, 100)
expect(shouldFitQuery).to.equal("One Two")
end)
it("should not truncate, off by one check", function()
local oneCharQuery = Text.Truncate("O", Enum.Font.SourceSans, 18, 100)
expect(oneCharQuery).to.equal("O")
end)
it("should truncate, off by one check", function()
local oneCharNoRoomQuery = Text.Truncate("O", Enum.Font.SourceSans, 18, 0)
expect(oneCharNoRoomQuery).to.equal("")
end)
it("should perform a negative width check", function()
local shouldFitQuery = Text.Truncate("One Two", Enum.Font.SourceSans, 18, -100, "...")
expect(shouldFitQuery).to.equal("")
end)
itFIXME("should truncate long graphemes properly", function()
-- 11-byte rainbow flag grapheme
-- Flag, zero-space-joiner, rainbow
local rainbowFlag = utf8.char(127987) .. utf8.char(8205) .. utf8.char(127752)
local oneFlagWithinLimit = Text.Truncate(
rainbowFlag, Enum.Font.SourceSans, 18, 100, "...")
expect(oneFlagWithinLimit).to.equal(rainbowFlag)
local twoRainbowFlags = rainbowFlag .. rainbowFlag
local twoFlagsAreFine = Text.Truncate(
twoRainbowFlags, Enum.Font.SourceSans, 18, 100, "...")
expect(twoFlagsAreFine).to.equal(twoRainbowFlags)
local fourRainbowFlags = twoRainbowFlags .. twoRainbowFlags
local fourFlagsIsTooLong = Text.Truncate(
fourRainbowFlags, Enum.Font.SourceSans, 18, 100, "...")
expect(fourFlagsIsTooLong).to.equal(twoRainbowFlags .. "...") -- With --fflags==true fails because of truncation
end)
end)
describe("TruncateTextLabel", function()
it("should use text label attributes to truncate text", function()
local screenGui = Instance.new("ScreenGui")
local textLabel = Instance.new("TextLabel")
textLabel.Size = UDim2.new(0, 100, 0, 32)
textLabel.Text = "One Two Three Four Five Six Seven Eight Nine Ten Eleven Twelve"
textLabel.Font = Enum.Font.SourceSans
textLabel.TextSize = 18
textLabel.Parent = screenGui
Text.TruncateTextLabel(textLabel)
expect(textLabel.Text).to.equal("One Two Three ")
end)
end)
describe("TrimString", function()
it("Should trim the string properly 1", function()
local trimmedInput = Text.Trim("")
local expected = ""
expect(trimmedInput).to.equal(expected)
end)
it("Should trim the string properly 2", function()
local trimmedInput = Text.Trim(" ")
local expected = ""
expect(trimmedInput).to.equal(expected)
end)
it("Should trim the string properly 3", function()
local trimmedInput = Text.Trim("ab")
local expected = "ab"
expect(trimmedInput).to.equal(expected)
end)
it("Should trim the string properly 4", function()
local trimmedInput = Text.Trim(" ab ")
local expected = "ab"
expect(trimmedInput).to.equal(expected)
end)
it("Should trim the string properly 5", function()
local trimmedInput = Text.Trim(" a b ")
local expected = "a b"
expect(trimmedInput).to.equal(expected)
end)
it("Should trim the string properly 6", function()
local trimmedInput = Text.Trim("\r\n\t\f a\r\n\t\f ")
local expected = "a"
expect(trimmedInput).to.equal(expected)
end)
it("Should trim the string with unicode characters properly", function()
local trimmedInput = Text.Trim("😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓")
local expected = "😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓"
expect(trimmedInput).to.equal(expected)
end)
it("Should trim the string properly 7", function()
local trimmedInput = Text.Trim(" 😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓 ")
local expected = "😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓"
expect(trimmedInput).to.equal(expected)
end)
it("Should trim the string properly 8", function()
local trimmedInput = Text.Trim("\n 😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓 \n")
local expected = "😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓"
expect(trimmedInput).to.equal(expected)
end)
end)
describe("RightTrimString", function()
it("Should right trim the string properly 1", function()
local trimmedInput = Text.RightTrim("")
local expected = ""
expect(trimmedInput).to.equal(expected)
end)
it("Should right trim the string properly 2", function()
local trimmedInput = Text.RightTrim(" ")
local expected = ""
expect(trimmedInput).to.equal(expected)
end)
it("Should right trim the string properly 3", function()
local trimmedInput = Text.RightTrim("ab")
local expected = "ab"
expect(trimmedInput).to.equal(expected)
end)
it("Should right trim the string properly 4", function()
local trimmedInput = Text.RightTrim(" ab ")
local expected = " ab"
expect(trimmedInput).to.equal(expected)
end)
it("Should right trim the string properly 5", function()
local trimmedInput = Text.RightTrim(" a b ")
local expected = " a b"
expect(trimmedInput).to.equal(expected)
end)
it("Should right trim the string properly 6", function()
local trimmedInput = Text.RightTrim("\r\n\t\f a\r\n\t\f ")
local expected = "\r\n\t\f a"
expect(trimmedInput).to.equal(expected)
end)
it("Should right trim the string with unicode characters properly", function()
local trimmedInput = Text.RightTrim("😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓")
local expected = "😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓"
expect(trimmedInput).to.equal(expected)
end)
it("Should right trim the string properly 7", function()
local trimmedInput = Text.RightTrim(" 😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓 ")
local expected = " 😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓"
expect(trimmedInput).to.equal(expected)
end)
it("Should right trim the string properly 8", function()
local trimmedInput = Text.RightTrim("\n 😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓 \n")
local expected = "\n 😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓"
expect(trimmedInput).to.equal(expected)
end)
end)
describe("LeftTrimString", function()
it("Should left trim the string properly 1", function()
local trimmedInput = Text.LeftTrim("")
local expected = ""
expect(trimmedInput).to.equal(expected)
end)
it("Should left trim the string properly 2", function()
local trimmedInput = Text.LeftTrim(" ")
local expected = ""
expect(trimmedInput).to.equal(expected)
end)
it("Should left trim the string properly 3", function()
local trimmedInput = Text.LeftTrim("ab")
local expected = "ab"
expect(trimmedInput).to.equal(expected)
end)
it("Should left trim the string properly 4", function()
local trimmedInput = Text.LeftTrim(" ab ")
local expected = "ab "
expect(trimmedInput).to.equal(expected)
end)
it("Should left trim the string properly 5", function()
local trimmedInput = Text.LeftTrim(" a b ")
local expected = "a b "
expect(trimmedInput).to.equal(expected)
end)
it("Should left trim the string properly 6", function()
local trimmedInput = Text.LeftTrim("\r\n\t\f a\r\n\t\f ")
local expected = "a\r\n\t\f "
expect(trimmedInput).to.equal(expected)
end)
it("Should left trim the string with unicode characters properly", function()
local trimmedInput = Text.LeftTrim("😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓")
local expected = "😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓"
expect(trimmedInput).to.equal(expected)
end)
it("Should left trim the string properly 7", function()
local trimmedInput = Text.LeftTrim(" 😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓 ")
local expected = "😤👩🏼‍🏫😭ぼ😀で😹🤕あ👩🏻‍🎓 "
expect(trimmedInput).to.equal(expected)
end)
it("Should left trim the string properly", function()
local trimmedInput = Text.LeftTrim("\n 😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓 \n")
local expected = "😤👩🏼‍🏫😭ぼ😀 \nで😹🤕あ👩🏻‍🎓 \n"
expect(trimmedInput).to.equal(expected)
end)
end)
describe("SpaceNormalize", function()
it("should remove multiple spaces between words", function()
local a = "This is not a normal sentence."
expect(Text.SpaceNormalize(a)).to.equal("This is not a normal sentence.")
end)
it("should remove leading and trailing whitespace", function()
local a = " SpaceTabSpaceTab "
expect(Text.SpaceNormalize(a)).to.equal("SpaceTabSpaceTab")
end)
it("should not change a string with no whitespace", function()
local a = "There'sNo%Whit.e\\space--InThis."
expect(Text.SpaceNormalize(a)).to.equal(a)
end)
it("should remove all whitespace in a string that is nothing but whitespace", function()
local a = " "
expect(Text.SpaceNormalize(a)).to.equal("")
end)
it("should handle the case where the string is empty", function()
local a = ""
expect(Text.SpaceNormalize(a)).to.equal(a)
end)
it("should throw an error if called an a non-string type", function()
local a = { first = 1, second = 2 }
expect(function()
Text.SpaceNormalize(a)
end).to.throw()
end)
end)
describe("Split", function()
local function tableEquals(tb1, tb2)
local tables = { tb1, tb2 }
for _,tb in ipairs(tables) do
for key in pairs(tb) do
if tb1[key] ~= tb2[key] then
return false
end
end
end
return true
end
it("should return the correct table for your standard use case", function()
local a = "this,is,comma,separated"
local pattern = ","
local expectedResult = {
[1] = "this",
[2] = "is",
[3] = "comma",
[4] = "separated",
}
expect(tableEquals(Text.Split(a, pattern), expectedResult)).to.equal(true)
end)
it("should not remove whitespace", function()
local a = " SpaceTab , , Space"
local pattern = ","
local expectedResult = {
[1] = " SpaceTab ",
[2] = " ",
[3] = " Space",
}
expect(tableEquals(Text.Split(a, pattern), expectedResult)).to.equal(true)
end)
it("should treat regular expressions as plain text", function()
local a = "Notyour^%s+normalstring.Thisisasecondsentence."
local b = "."
local c = "^%s+"
local d = "%A"
local expectedB = {
[1] = "Notyour^%s+normalstring",
[2] = "Thisisasecondsentence",
[3] = "",
}
local expectedC = {
[1] = "Notyour",
[2] = "normalstring.Thisisasecondsentence."
}
local expectedD = {
[1] = "Notyour^%s+normalstring.Thisisasecondsentence."
}
expect(tableEquals(Text.Split(a, b), expectedB)).to.equal(true)
expect(tableEquals(Text.Split(a, c), expectedC)).to.equal(true)
expect(tableEquals(Text.Split(a, d), expectedD)).to.equal(true)
end)
it("should work when pattern is not in string", function()
local a = "The pattern you are looking for does not exist."
local pattern = ","
local expectedResult = {
[1] = "The pattern you are looking for does not exist.",
}
expect(tableEquals(Text.Split(a, pattern), expectedResult)).to.equal(true)
end)
it("should work when called on an empty string", function()
local a = ""
local pattern = ","
local expectedResult = {
[1] = "",
}
expect(tableEquals(Text.Split(a, pattern), expectedResult)).to.equal(true)
end)
it("should throw an error if called on an empty pattern", function()
local a = "The pattern definitely doesn't exist here."
local pattern = ""
expect(function()
Text.Split(a, pattern)
end).to.throw()
end)
it("should throw an error if called an a non-string type", function()
local a = { first = 1, second = 2 }
local b = "an actual string"
expect(function()
Text.Split(a, b)
end).to.throw()
expect(function()
Text.Split(b, a)
end).to.throw()
end)
end)
end
@@ -0,0 +1,68 @@
--[[
memoize creates a function as a wrapper that caches the last outputs of a function.
This is useful if you know that the function should return the same output every
time it is run with the same inputs. The function should only return an output, and
not have any side effects. These side effects are not cached.
Without memoize's caching, even though the function ouputs the same values, the
memory locations of the values are different; tables made in the function, even if
they have the same values, won't be the same tables.
memoize only caches the last set of inputs and ouputs. This means that it is only
helpful when the function is likely to be called with the same inputs multiple
times in a row. This is the case with most Roact use cases.
Note that memoize only does a ** shallow check on table inputs ** . This means
that if the same table is input but the elements of the table are different then
it will be assumed that the table has not changed.
In addition to all the previous warnings, memoize strips trailing nils. This means
that if foo is a memoized function and we call foo(), then foo(nil) will return a
cached value. This is opposed to how print handles input. print() only outputs a
new line, but print(nil) outputs "nil". This is because varargs can detect the
number of arguments passed in. So, be careful when using memoize with varargs.
Trailing nils will be stripped.
The wrapper can take any number of inputs and give any number of outputs.
Leading and interspersed nils are handled gracefully. Trailing nils on the input
are stripped.
]]
local function captureSize(...)
return {...}, select("#", ...)
end
local function memoize(func)
@@ -0,0 +1,213 @@
return function()
local memoize = require(script.Parent.memoize)
describe("memoize", function()
it("should handle arity 0", function()
local callCount = 0
local identity = memoize(function(a, b)
callCount = callCount + 1
return a, b
end)
expect(identity()).to.equal(nil)
expect(identity(nil)).to.equal(nil)
expect(identity(nil, nil)).to.equal(nil)
expect(callCount).to.equal(1)
end)
it("should handle arity 1", function()
local callCount = 0
local identity = memoize(function(a)
callCount = callCount + 1
return a
end)
expect(identity(5)).to.equal(5)
expect(identity(5)).to.equal(5)
expect(callCount).to.equal(1)
expect(identity(6)).to.equal(6)
expect(callCount).to.equal(2)
expect(identity(5)).to.equal(5)
expect(callCount).to.equal(3)
end)
it("should handle arity 2", function()
local callCount = 0
local identity = memoize(function(a, b)
callCount = callCount + 1
return a, b
end)
local a, b
a, b = identity(5, 6)
expect(a).to.equal(5)
expect(b).to.equal(6)
a, b = identity(5, 6)
expect(a).to.equal(5)
expect(b).to.equal(6)
expect(callCount).to.equal(1)
a, b = identity(6, 5)
expect(a).to.equal(6)
expect(b).to.equal(5)
expect(callCount).to.equal(2)
a, b = identity(5, 6)
expect(a).to.equal(5)
expect(b).to.equal(6)
expect(callCount).to.equal(3)
end)
it("should handle mixed arity", function()
local callCount = 0
local identity = memoize(function(a, b)
callCount = callCount + 1
return a, b
end)
local a, b
a, b = identity(5, 6)
expect(a).to.equal(5)
expect(b).to.equal(6)
a, b = identity(5, 6)
expect(a).to.equal(5)
expect(b).to.equal(6)
expect(callCount).to.equal(1)
a, b = identity(5)
expect(a).to.equal(5)
expect(b).to.equal(nil)
a, b = identity(5)
expect(a).to.equal(5)
expect(b).to.equal(nil)
expect(callCount).to.equal(2)
a, b = identity()
expect(a).to.equal(nil)
expect(b).to.equal(nil)
a, b = identity()
expect(a).to.equal(nil)
expect(b).to.equal(nil)
expect(callCount).to.equal(3)
end)
it("should handle trailing nils", function()
local callCount = 0
local identity = memoize(function(a, b)
callCount = callCount + 1
return a, b
end)
local a, b
a, b = identity(5, nil)
expect(a).to.equal(5)
expect(b).to.equal(nil)
a, b = identity(5)
expect(a).to.equal(5)
expect(b).to.equal(nil)
expect(callCount).to.equal(1)
a, b = identity(7)
expect(a).to.equal(7)
expect(b).to.equal(nil)
expect(callCount).to.equal(2)
a, b = identity(5)
expect(a).to.equal(5)
expect(b).to.equal(nil)
expect(callCount).to.equal(3)
end)
it("should handle leading nils", function()
local callCount = 0
local identity = memoize(function(a, b)
callCount = callCount + 1
return a, b
end)
local a, b
a, b = identity(nil, 7)
expect(a).to.equal(nil)
expect(b).to.equal(7)
a, b = identity(nil, 7)
expect(a).to.equal(nil)
expect(b).to.equal(7)
expect(callCount).to.equal(1)
a, b = identity(7)
expect(a).to.equal(7)
expect(b).to.equal(nil)
expect(callCount).to.equal(2)
a, b = identity(nil, 7)
expect(a).to.equal(nil)
expect(b).to.equal(7)
expect(callCount).to.equal(3)
end)
it("should handle interspersed nils", function()
local callCount = 0
local identity = memoize(function(a, b, c, d)
callCount = callCount + 1
return a, b, c, d
end)
local a, b, c, d
a, b, c, d = identity(7, nil, 7, nil)
expect(a).to.equal(7)
expect(b).to.equal(nil)
expect(c).to.equal(7)
expect(d).to.equal(nil)
-- Trailing nils can affect how interspersed nils are handled
a, b, c, d = identity(7, nil, 7)
expect(a).to.equal(7)
expect(b).to.equal(nil)
expect(c).to.equal(7)
expect(d).to.equal(nil)
expect(callCount).to.equal(1)
a, b, c, d = identity(7, nil, nil, nil)
expect(a).to.equal(7)
expect(b).to.equal(nil)
expect(c).to.equal(nil)
expect(d).to.equal(nil)
expect(callCount).to.equal(2)
a, b, c, d = identity(7, nil, 7, nil)
expect(a).to.equal(7)
expect(b).to.equal(nil)
expect(c).to.equal(7)
expect(d).to.equal(nil)
expect(callCount).to.equal(3)
end)
end)
end
@@ -0,0 +1,5 @@
{
"lint": {
"LocalShadow": "fatal"
}
}
@@ -0,0 +1,8 @@
{
"language": {
"mode": "nonstrict"
},
"lint": {
"ImplicitReturn": "fatal"
}
}
@@ -0,0 +1,8 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action(script.Name, function(user)
return {
user = user
}
end)
@@ -0,0 +1,8 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action(script.Name, function(users)
return {
users = users
}
end)
@@ -0,0 +1,8 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action(script.Name, function(userId)
return {
userId = userId
}
end)
@@ -0,0 +1,9 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action(script.Name, function(userId, response)
return {
userId = userId,
response = response
}
end)
@@ -0,0 +1,8 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action(script.Name, function(userId)
return {
userId = userId
}
end)
@@ -0,0 +1,8 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action(script.Name, function(placesInfos)
return {
placesInfos = placesInfos,
}
end)
@@ -0,0 +1,8 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action(script.Name, function(userId)
return {
userId = userId,
}
end)
@@ -0,0 +1,8 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action(script.Name, function(deviceOrientation)
return {
deviceOrientation = deviceOrientation,
}
end)
@@ -0,0 +1,10 @@
local CorePackages = game:GetService("CorePackages")
local Common = CorePackages.AppTempCommon.Common
local Action = require(Common.Action)
return Action(script.Name, function(count)
return {
count = count,
}
end)
@@ -0,0 +1,14 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
local ArgCheck = require(CorePackages.ArgCheck)
--[[
Each entry in the table is a type of GameIcon with the universe id as key
]]
return Action(script.Name, function(iconsTable)
ArgCheck.isType(iconsTable, "table", "iconsTable")
return {
gameIcons = iconsTable
}
end)
@@ -0,0 +1,27 @@
return function()
local SetGameIcons = require(script.Parent.SetGameIcons)
it("should assert if given a non-table for thumbnailsTable", function()
SetGameIcons({})
expect(function()
SetGameIcons("string")
end).to.throw()
expect(function()
SetGameIcons(0)
end).to.throw()
expect(function()
SetGameIcons(nil)
end).to.throw()
expect(function()
SetGameIcons(false)
end).to.throw()
expect(function()
SetGameIcons(function() end)
end).to.throw()
end)
end
@@ -0,0 +1,26 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
--[[
Passes a table that looks like this : { "universeId" : {json}, ... }
{
"26034470" : {
universeId : "26034470",
placeId : "70542190",
url : https://t5.rbxcdn.com/ed422c6fbb22280971cfb289f40ac814,
final : true
}, {...}, ...
}
]]
--TODO MOBLUAPP-778 Refactor improper Setter Actions.
return Action(script.Name, function(thumbnailsTable)
assert(type(thumbnailsTable) == "table",
string.format("SetGameThumbnails action expects thumbnailsTable to be a table, was %s", type(thumbnailsTable)))
return {
thumbnails = thumbnailsTable
}
end)
@@ -0,0 +1,11 @@
local CorePackages = game:GetService("CorePackages")
local Common = CorePackages.AppTempCommon.Common
local Action = require(Common.Action)
return Action(script.Name, function(userId, universeId)
return {
userId = userId,
universeId = universeId,
}
end)
@@ -0,0 +1,9 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action(script.Name, function(userId, isFriend)
return {
userId = userId,
isFriend = isFriend,
}
end)
@@ -0,0 +1,11 @@
local CorePackages = game:GetService("CorePackages")
local Common = CorePackages.AppTempCommon.Common
local Action = require(Common.Action)
return Action(script.Name, function(userId, membershipType)
return {
userId = userId,
membershipType = membershipType,
}
end)
@@ -0,0 +1,10 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action(script.Name, function(userId, presence, lastLocation)
return {
userId = tostring(userId),
presence = presence,
lastLocation = lastLocation,
}
end)
@@ -0,0 +1,13 @@
local CorePackages = game:GetService("CorePackages")
local Common = CorePackages.AppTempCommon.Common
local Action = require(Common.Action)
return Action(script.Name, function(userId, image, thumbnailType, thumbnailSize)
return {
userId = userId,
image = image,
thumbnailType = thumbnailType,
thumbnailSize = thumbnailSize,
}
end)
@@ -0,0 +1,9 @@
local CorePackages = game:GetService("CorePackages")
local Action = require(CorePackages.AppTempCommon.Common.Action)
return Action(script.Name, function(key, status)
return {
key = key,
status = status
}
end)
@@ -0,0 +1,21 @@
return function()
local CorePackages = game:GetService("CorePackages")
local UpdateFetchingStatus = require(CorePackages.AppTempCommon.LuaApp.Actions.UpdateFetchingStatus)
describe("Action UpdateFetchingStatus", function()
it("should return correct action name", function()
expect(UpdateFetchingStatus.name).to.equal("UpdateFetchingStatus")
end)
it("should return correct action type name", function()
local action = UpdateFetchingStatus()
expect(action.type).to.equal(UpdateFetchingStatus.name)
end)
it("should return a table with the correct key and status", function()
local action = UpdateFetchingStatus("key", "status")
expect(action.key).to.equal("key")
expect(action.status).to.equal("status")
end)
end)
end
@@ -0,0 +1,8 @@
{
"language": {
"mode": "nonstrict"
},
"lint": {
"ImplicitReturn": "fatal"
}
}
@@ -0,0 +1,51 @@
local Workspace = game:GetService("Workspace")
local RunService = game:GetService('RunService')
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
local BAR_SLICE_CENTER = Rect.new(1, 0, 2, 3)
local BAR_MAX_SIZE = 15
local BAR_MAX_AMPLITUDE = 40
local BAR_DIAMETER = 4
local BAR_PERIOD = 1.25
local LoadingBar = Roact.Component:extend("LoadingBar")
function LoadingBar:init()
self.barRef = Roact.createRef()
end
function LoadingBar:render()
local zIndex = self.props.ZIndex
return Roact.createElement("ImageLabel", {
Image = "rbxasset://textures/ui/LuaApp/9-slice/gr-loading-indicator.png",
ScaleType = "Slice",
SliceCenter = BAR_SLICE_CENTER,
BackgroundTransparency = 1,
BorderSizePixel = 0,
ZIndex = zIndex,
[Roact.Ref] = self.barRef,
})
end
function LoadingBar:didMount()
self.connection = RunService.RenderStepped:Connect(function()
local t = Workspace.DistributedGameTime
local instance = self.barRef.current
local period = 2.0 * math.pi / BAR_PERIOD
local width = (BAR_MAX_SIZE/2) * (1 - math.cos(2*t*period))
instance.Size = UDim2.new(0, BAR_DIAMETER + width, 0, BAR_DIAMETER)
local x = BAR_MAX_AMPLITUDE * math.cos(t*period)
instance.Position = UDim2.new(0.5, x - width/2 - BAR_DIAMETER/2, 0.5, 0)
end)
end
function LoadingBar:willUnmount()
self.connection:Disconnect()
end
return LoadingBar
@@ -0,0 +1,17 @@
return function()
local LoadingBar = require(script.Parent.LoadingBar)
local CorePackages = game:GetService("CorePackages")
local Roact = require(CorePackages.Roact)
it("should create and destroy without errors", function()
local element = Roact.createElement(LoadingBar, {
Position = UDim2.new(0.5, 0, 0.5, 5),
AnchorPoint = Vector2.new(0.5, 0.5),
})
local instance = Roact.mount(element)
Roact.unmount(instance)
end)
end
@@ -0,0 +1,8 @@
{
"language": {
"mode": "nonstrict"
},
"lint": {
"ImplicitReturn": "fatal"
}
}
@@ -0,0 +1,4 @@
return {
AvatarThumbnail = "AvatarThumbnail",
HeadShot = "HeadShot",
}
@@ -0,0 +1,21 @@
local RetrievalStatus = {}
local EnumValues =
{
NotStarted = "NotStarted",
Fetching = "Fetching",
Done = "Done",
Failed = "Failed",
}
setmetatable(RetrievalStatus,
{
__newindex = function(t, key, index)
end,
__index = function(t, index)
assert(EnumValues[index] ~= nil, ("RetrievalStatus Enum has no value: " .. tostring(index)))
return EnumValues[index]
end
})
return RetrievalStatus
@@ -0,0 +1,10 @@
local CorePackages = game:GetService("CorePackages")
local User = require(CorePackages.AppTempCommon.LuaApp.Models.User)
return {
[0] = User.PresenceType.OFFLINE,
[1] = User.PresenceType.ONLINE,
[2] = User.PresenceType.IN_GAME,
[3] = User.PresenceType.IN_STUDIO,
}
@@ -0,0 +1,8 @@
{
"language": {
"mode": "nonstrict"
},
"lint": {
"ImplicitReturn": "fatal"
}
}
@@ -0,0 +1,15 @@
local CorePackages = game:GetService("CorePackages")
local ThrottleUserId = require(CorePackages.AppTempCommon.LuaApp.Utils.ThrottleUserId)
local FIntAvatarEditorNewCatalogButton = settings():GetFVariable("AvatarEditorNewCatalogButton2")
return function(userId)
if tonumber(userId) then
local throttleNumber = tonumber(FIntAvatarEditorNewCatalogButton)
local id = tonumber(userId)
return ThrottleUserId(throttleNumber, id)
else
return false
end
end
@@ -0,0 +1,11 @@
-- TODO: Delete this file when deleting the flag: LuaAppConvertUniverseIdToStringV364
local FFlagLuaAppConvertUniverseIdToString = settings():GetFFlag("LuaAppConvertUniverseIdToStringV364")
return function(universeId)
-- When the flag is on, we've converted the universe id to string at the place we received it
if FFlagLuaAppConvertUniverseIdToString then
return universeId
else
return tostring(universeId)
end
end
@@ -0,0 +1,15 @@
local CorePackages = game:GetService("CorePackages")
local Players = game:GetService("Players")
local ThrottleUserId = require(CorePackages.AppTempCommon.LuaApp.Utils.ThrottleUserId)
local FIntEnableFriendFooterOnHomePage = settings():GetFVariable("EnableFriendFooterOnHomePageV369")
-- Don't call this function globally because we cannot get the userId
-- Reason: The LocalPlayer wouldn't be ready if we called it globally.
return function()
local throttleNumber = tonumber(FIntEnableFriendFooterOnHomePage)
local userId = Players.LocalPlayer.UserId
return ThrottleUserId(throttleNumber, userId)
end
@@ -0,0 +1,5 @@
game:DefineFastFlag("LuaAppFixLightTheme", false)
return function()
return game:GetFastFlag("LuaAppFixLightTheme")
end
@@ -0,0 +1,3 @@
return function()
return settings():GetFFlag("UseDateTimeType3")
end
@@ -0,0 +1,8 @@
{
"language": {
"mode": "nonstrict"
},
"lint": {
"ImplicitReturn": "fatal"
}
}
@@ -0,0 +1,19 @@
local CorePackages = game:GetService("CorePackages")
local HttpService = game:GetService("HttpService")
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
return function(requestImpl, conversationId, universeId, decorators)
assert(requestImpl, "requestImpl is required")
assert(conversationId, "conversationId is required")
assert(universeId, "universeId is required")
local payload = HttpService:JSONEncode({
conversationId = conversationId,
universeId = universeId,
decorators = decorators
})
local url = string.format("%s/send-game-link-message", Url.CHAT_URL)
return requestImpl(url, "POST", { postBody = payload })
end
@@ -0,0 +1,16 @@
local CorePackages = game:GetService("CorePackages")
local HttpService = game:GetService("HttpService")
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
return function(requestImpl, conversationId, messageText, decorators)
local payload = HttpService:JSONEncode({
conversationId = conversationId,
message = messageText,
decorators = decorators
})
local url = string.format("%s/send-message", Url.CHAT_URL)
return requestImpl(url, "POST", { postBody = payload })
end
@@ -0,0 +1,14 @@
local CorePackages = game:GetService("CorePackages")
local HttpService = game:GetService("HttpService")
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
return function(requestImpl, userId, clientId)
local payload = HttpService:JSONEncode({
participantuserId = userId
})
local url = string.format("%s/start-one-to-one-conversation", Url.CHAT_URL)
return requestImpl(url, "POST", { postBody = payload })
end
@@ -0,0 +1,25 @@
local CorePackages = game:GetService("CorePackages")
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
--[[
Docs: https://thumbnails.roblox.com/docs#!/Games/get_v1_games_icons
This resolves to
{
"data": [
{
"targetId": 0,
"state": "Error",
"imageUrl": "string"
}
]
}
]]
return function (requestImpl, universeIds, size)
local qs = Url:makeQueryString({
universeIds = table.concat(universeIds, ","),
format = "png",
size = size,
})
local url = string.format("%sv1/games/icons?%s", Url.THUMBNAILS_URL, qs)
return requestImpl(url, "GET")
end
@@ -0,0 +1,54 @@
--[[
*** DEPRECATED ***
TODO: removed this file after new thumbnail API is being in use without any flags
RELATED: GAMEDISC-27 GAMEDISC-126 FIntLuaAppPercentRollOutNewThumbnailsApiV3
]]
local CorePackages = game:GetService("CorePackages")
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
--[[
This endpoint returns a promise that resolves to:
[
{
"final": true,
"url": "string",
"retryToken": "string",
"universeId": 0,
"placeId": 0
}, {...}, ...
]
]]
-- requestImpl - (function<promise<HttpResponse>>(url, requestMethod, options))
-- imageTokens - (array<long>) the placeIds of the places you want to get thumbnails for
-- height - (int) the height of the asset to render
-- width - (int) the width of the asset to render
return function(requestImpl, imageTokens, height, width)
local args = {}
if height then
table.insert(args, string.format("height=%d", height))
end
if width then
table.insert(args, string.format("width=%d", width))
end
-- append all of the thumbnail tokens
local totalTokens = 0
for _, value in pairs(imageTokens) do
totalTokens = totalTokens + 1
table.insert(args, string.format("imageTokens=%s", value))
end
if totalTokens == 0 then
error("cannot fetch thumbnails without tokens")
end
-- construct the url
local url = string.format("%sv1/games/game-thumbnails?%s", Url.GAME_URL, table.concat(args, "&"))
-- return a promise of the result listed above
return requestImpl(url, "GET")
end
@@ -0,0 +1,33 @@
local CorePackages = game:GetService("CorePackages")
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
--[[
This endpoint returns games' information with a batches of place ids
Doc: https://games.roblox.com/docs#!/Games/get_v1_games_multiget_place_details
{
"placeId": 0,
"name": "string",
"description": "string",
"url": "string",
"builder": "string",
"builderId": 0,
"isPlayable": true,
"reasonProhibited": "string",
"universeId": 0,
"universeRootPlaceId": 0,
"price": 0,
"imageToken": "string"
}
]]--
return function(requestImpl, placeIds)
local argTable = {
placeIds = placeIds,
}
local args = Url:makeQueryString(argTable)
local url = string.format("%s/v1/games/multiget-place-details?%s", Url.GAME_URL, args)
return requestImpl(url, "GET")
end
@@ -0,0 +1,17 @@
local CorePackages = game:GetService("CorePackages")
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
return function(requestImpl, placeIds)
local argTable = {
placeIds = placeIds,
}
-- construct the url
local args = Url:makeQueryString(argTable)
local url = string.format("%s/v1/games/multiget-place-details?%s",
Url.GAME_URL, args
)
return requestImpl(url, "GET")
end
@@ -0,0 +1,39 @@
local CorePackages = game:GetService("CorePackages")
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
--[[
Documentation of endpoint:
https://thumbnails.roblox.com/docs#!/Avatar/get_v1_users_avatar
input:
userIds
thumbnailSize
output:
[
{
"targetId": number,
"state": string,
"imageUrl": string,
},
]
]]
local MAX_USER_IDS = 100
return function (networkImpl, userIds, thumbnailSize)
assert(type(userIds) == "table", "ThumbnailsGetAvatar expects userIds to be a table")
if #userIds == 0 or #userIds > MAX_USER_IDS then
error(string.format("ThumbnailsGetAvatar request expects userIds count between 1-%d", MAX_USER_IDS))
end
local queryString = Url:makeQueryString({
userIds = table.concat(userIds, ","),
size = thumbnailSize,
format = "png",
})
local url = string.format("%sv1/users/avatar?%s", Url.THUMBNAILS_URL, queryString)
return networkImpl(url, "GET")
end
@@ -0,0 +1,39 @@
local CorePackages = game:GetService("CorePackages")
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
--[[
Documentation of endpoint:
https://thumbnails.roblox.com/docs#!/Avatar/get_v1_users_avatar_headshot
input:
userIds
thumbnailSize
output:
[
{
"targetId": number,
"state": string,
"imageUrl": string,
},
]
]]
local MAX_USER_IDS = 100
return function (networkImpl, userIds, thumbnailSize)
assert(type(userIds) == "table", "ThumbnailsGetAvatarHeadshot expects userIds to be a table")
if #userIds == 0 or #userIds > MAX_USER_IDS then
error(string.format("ThumbnailsGetAvatarHeadshot request expects userIds count between 1-%d", MAX_USER_IDS))
end
local queryString = Url:makeQueryString({
userIds = table.concat(userIds, ","),
size = thumbnailSize,
format = "png",
})
local url = string.format("%sv1/users/avatar-headshot?%s", Url.THUMBNAILS_URL, queryString)
return networkImpl(url, "GET")
end
@@ -0,0 +1,31 @@
local CorePackages = game:GetService("CorePackages")
local Players = game:GetService("Players")
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
local isNewFriendsEndpointsEnabled = require(CorePackages.AppTempCommon.LuaChat.Flags.isNewFriendsEndpointsEnabled)
--[[
This endpoint returns a promise that resolves to:
[
{
"success:" true,
"count": "0"
},
]
]]--
-- requestImpl - (function<promise<HttpResponse>>(url, requestMethod, options))
return function(requestImpl)
local url = string.format("%s/user/get-friendship-count?%s",
Url.API_URL, tostring(Players.LocalPlayer.UserId)
)
if isNewFriendsEndpointsEnabled() then
url = string.format("%s/my/friends/count", Url.FRIEND_URL)
end
return requestImpl(url, "GET")
end
@@ -0,0 +1,10 @@
local CorePackages = game:GetService("CorePackages")
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
return function(requestImpl, userId)
local url = string.format("%s/users/%s/friends",
Url.FRIEND_URL, userId
)
return requestImpl(url, "GET")
end
@@ -0,0 +1,25 @@
local CorePackages = game:GetService("CorePackages")
local HttpService = game:GetService("HttpService")
local Url = require(CorePackages.AppTempCommon.LuaApp.Http.Url)
-- Endpoint documented here:
-- https://presence.roblox.com/docs
return function(requestImpl, userIds)
local userIdsToNumber = {}
for _, id in pairs(userIds) do
local idToNumber = tonumber(id)
if idToNumber then
table.insert(userIdsToNumber, idToNumber)
end
end
local payload = HttpService:JSONEncode({
userIds = userIdsToNumber,
})
local url = string.format("%s/presence/users", Url.PRESENCE_URL)
return requestImpl(url, "POST", { postBody = payload })
end
@@ -0,0 +1,48 @@
local CorePackages = game:GetService("CorePackages")
local Players = game:GetService("Players")
local Promise = require(CorePackages.AppTempCommon.LuaApp.Promise)
local THUMBNAIL_TYPE_BY_NAME = {
AvatarThumbnail = Enum.ThumbnailType.AvatarThumbnail,
HeadShot = Enum.ThumbnailType.HeadShot,
}
local THUMBNAIL_SIZE_BY_NAME = {
Size48x48 = Enum.ThumbnailSize.Size48x48,
Size60x60 = Enum.ThumbnailSize.Size60x60,
Size100x100 = Enum.ThumbnailSize.Size100x100,
Size150x150 = Enum.ThumbnailSize.Size150x150,
Size352x352 = Enum.ThumbnailSize.Size352x352
}
return function(userId, thumbnailType, thumbnailSize)
return Promise.new(function(resolve, reject)
--Async methods will yield the thread
spawn(function()
local result = {success = false}
local success, message = pcall(function()
local image, isFinal = Players:GetUserThumbnailAsync(
tonumber(userId), THUMBNAIL_TYPE_BY_NAME[thumbnailType], THUMBNAIL_SIZE_BY_NAME[thumbnailSize]
)
result = {
success = true,
id = userId,
thumbnailType = thumbnailType,
thumbnailSize = thumbnailSize,
image = isFinal and image or nil,
isFinal = isFinal,
}
end)
if success then
resolve(result)
else
result.message = message
reject(result)
end
end)
end)
end
@@ -0,0 +1,181 @@
--[[
Url Constructor
Provides a single location for base urls.
]]--
local ContentProvider = game:GetService("ContentProvider")
local FFlagLuaFixEconomyCreatorStatsUrl = game:DefineFastFlag("LuaFixEconomyCreatorStatsUrl", false)
-- helper functions
local function parseBaseUrlInformation()
-- get the current base url from the current configuration
local baseUrl = ContentProvider.BaseUrl
-- keep a copy of the base url (https://www.roblox.com/)
-- append a trailing slash if there isn't one
if baseUrl:sub(#baseUrl) ~= "/" then
baseUrl = baseUrl .. "/"
end
-- parse out scheme (http, https)
local _, schemeEnd = baseUrl:find("://")
-- parse out the prefix (www, kyle, ying, etc.)
local prefixIndex, prefixEnd = baseUrl:find("%.", schemeEnd + 1)
local basePrefix = baseUrl:sub(schemeEnd + 1, prefixIndex - 1)
-- parse out the domain (roblox.com/, sitetest1.robloxlabs.com/, etc.)
local baseDomain = baseUrl:sub(prefixEnd + 1)
return baseUrl, basePrefix, baseDomain
end
local function preventTableModification(aTable, key, value)
error("Attempt to modify read-only table")
end
local function createReadOnlyTable(aTable)
return setmetatable({}, {
__index = aTable,
__newindex = preventTableModification,
__metatable = false
});
end
-- url construction building blocks
local _baseUrl, _basePrefix, _baseDomain = parseBaseUrlInformation()
-- construct urls once
local _baseApiUrl = string.format("https://api.%s", _baseDomain)
local _baseApisUrl = string.format("https://apis.%s", _baseDomain)
local _baseAuthUrl = string.format("https://auth.%s", _baseDomain)
local _baseAccountSettingsUrl = string.format("https://accountsettings.%s", _baseDomain)
local _baseAvatarUrl = string.format("https://avatar.%s", _baseDomain)
local _baseCatalogUrl = string.format("https://catalog.%s", _baseDomain)
local _baseInventoryUrl = string.format("https://inventory.%s", _baseDomain)
local _baseChatUrl = string.format("https://chat.%sv2", _baseDomain)
local _baseFriendUrl = string.format("https://friends.%sv1", _baseDomain)
local _baseGameAssetUrl = string.format("https://assetgame.%s", _baseDomain)
local _baseGamesUrl = string.format("https://games.%s", _baseDomain)
local _baseGroupsUrl = string.format("https://groups.%s", _baseDomain)
local _baseNotificationUrl = string.format("https://notifications.%s", _baseDomain)
local _basePresenceUrl = string.format("https://presence.%sv1", _baseDomain)
local _baseRealtimeUrl = string.format("https://realtime.%s", _baseDomain)
local _baseWebUrl = string.format("https://web.%s", _baseDomain)
local _baseWwwUrl = string.format("https://www.%s", _baseDomain)
local _baseAdsUrl = string.format("https://ads.%s", _baseDomain)
local _baseFollowingsUrl = string.format("https://followings.%s", _baseDomain)
local _baseEconomyUrl = string.format("https://economy.%s", _baseDomain)
local _baseThumbnailsUrl = string.format("https://thumbnails.%s", _baseDomain)
local _baseAccountSettings = string.format("https://accountsettings.%s", _baseDomain)
local _basePremiumFeatures = string.format("https://premiumfeatures.%s", _baseDomain)
local _baseLocale = string.format("https://locale.%s", _baseDomain)
local _baseBadgesUrl = string.format("https://badges.%s", _baseDomain)
local _baseMetricsUrl = string.format("https://metrics.%sv1", _baseDomain)
local _baseApisRcsUrl = string.format("https://apis.rcs.%s", _baseDomain)
local _baseDiscussionsUrl = string.format("https://discussions.%s", _baseDomain)
local _baseContactsUrl = string.format("https://contacts.%s", _baseDomain)
local _baseSearchUrl = string.format("https://search.%s", _baseDomain)
local _baseStaticUrl = string.format("https://static.%s", _baseDomain)
local _baseGameSearchUITreatments = string.format("https://gamesearchuitreatments.api.%s", _baseDomain)
local _baseEconomyCreatorStats = FFlagLuaFixEconomyCreatorStatsUrl
and string.format("https://economycreatorstats.%s", _baseDomain)
or string.format("https://economycreatorstats.api.%s", _baseDomain)
local _baseUrlSecure = string.gsub(_baseUrl, "http://", "https://")
-- public api
local Url = {
DOMAIN = _baseDomain,
PREFIX = _basePrefix,
BASE_URL = _baseUrl,
BASE_URL_SECURE = _baseUrlSecure,
API_URL = _baseApiUrl,
APIS_URL = _baseApisUrl,
AUTH_URL = _baseAuthUrl,
ACCOUNT_SETTINGS_URL = _baseAccountSettingsUrl,
AVATAR_URL = _baseAvatarUrl,
CATALOG_URL = _baseCatalogUrl,
INVENTORY_URL = _baseInventoryUrl,
GAME_URL = _baseGamesUrl,
GAME_ASSET_URL = _baseGameAssetUrl,
GROUPS_URL = _baseGroupsUrl,
CHAT_URL = _baseChatUrl,
FRIEND_URL = _baseFriendUrl,
PRESENCE_URL = _basePresenceUrl,
NOTIFICATION_URL = _baseNotificationUrl,
REALTIME_URL = _baseRealtimeUrl,
WEB_URL = _baseWebUrl,
WWW_URL = _baseWwwUrl,
ADS_URL = _baseAdsUrl,
SEARCH_URL = _baseSearchUrl,
GAME_SEARCH_UI_TREATMENTS = _baseGameSearchUITreatments,
FOLLOWINGS_URL = _baseFollowingsUrl,
ECONOMY_URL = _baseEconomyUrl,
THUMBNAILS_URL = _baseThumbnailsUrl,
BADGES_URL = _baseBadgesUrl,
ACCOUNT_SETTINGS = _baseAccountSettings,
PREMIUM_FEATURES = _basePremiumFeatures,
LOCALE = _baseLocale,
METRICS_URL = _baseMetricsUrl,
APIS_RCS_URL = _baseApisRcsUrl,
DISCUSSIONS_URL = _baseDiscussionsUrl,
CONTACTS_URL = _baseContactsUrl,
STATIC_URL = _baseStaticUrl,
BLOG_URL = "https://blog.roblox.com/",
CORP_URL = "https://corp.roblox.com/",
ECNOMY_CREATOR_STATS = _baseEconomyCreatorStats,
}
function Url:getUserProfileUrl(userId)
return string.format("%susers/%s/profile", self.BASE_URL, userId)
end
function Url:getUserFriendsUrl(userId)
return string.format("%susers/%s/friends", self.BASE_URL, userId)
end
function Url:getUserInventoryUrl(userId)
return string.format("%susers/%s/inventory", self.BASE_URL, userId)
end
function Url:getPlaceDefaultThumbnailUrl(placeId, width, height)
return string.format(
"%sThumbs/Asset.ashx?width=%d&height=%d&assetId=%s&ignorePlaceMediaItems=true",
self.BASE_URL,
width,
height,
tostring(placeId))
end
function Url:isVanitySite()
return self.PREFIX ~= "www"
end
-- data - (table<string, string>) a table of key/value pairs to format
function Url:makeQueryString(data)
--NOTE - This function can be used to create a query string of parameters
-- at the end of url query, or create a application/form-url-encoded post body string
local params = {}
-- NOTE - Arrays are handled, but generally data is expected to be flat.
for key, value in pairs(data) do
if value ~= nil then --for optional params
if type(value) == "table" then
for i = 1, #value do
table.insert(params, key .. "=" .. value[i])
end
else
table.insert(params, key .. "=" .. tostring(value))
end
end
end
return table.concat(params, "&")
end
-- prevent anyone from modifying this table:
Url = createReadOnlyTable(Url)
return Url
@@ -0,0 +1,12 @@
return function()
local ContentProvider = game:GetService("ContentProvider")
local Url = require(script.Parent.Url)
it("The base url has not been changed for debugging", function()
local baseUrl = ContentProvider.BaseUrl
expect(baseUrl).to.equal(Url.BASE_URL)
end)
end
@@ -0,0 +1,17 @@
--[[
A function to return a fake ID, used for testing.
We turn all IDs into strings as we typically use them as keys in the state.
It's better to use a string than a number, because a number would indicate
an array index.
Roblox APIs expect to be given integers for IDs however, so just tonumber()
the ID in this case.
]]
local lastId = 0
return function()
lastId = lastId + 1
return tostring(lastId)
end
@@ -0,0 +1,5 @@
{
"language": {
"mode": "nonstrict"
}
}
@@ -0,0 +1,39 @@
--[[
{
universeId : string,
state : string,
url : string,
}
]]
local Thumbnail = {}
function Thumbnail.new()
local self = {}
return self
end
function Thumbnail.fromThumbnailData(thumbnailData, size)
local self = Thumbnail.new()
self.universeId = tostring(thumbnailData.targetId)
self.state = thumbnailData.state
self.url = thumbnailData.imageUrl
self.size = size
return self
end
function Thumbnail.isCompleteThumbnailData(thumbnailData)
return type(thumbnailData) == "table"
and type(thumbnailData.targetId) == "number"
and type(thumbnailData.state) == "string"
and (type(thumbnailData.imageUrl) == "string" or thumbnailData.imageUrl == nil)
end
function Thumbnail.checkStateIsFinal(thumbnailState)
return thumbnailState ~= "Pending"
end
return Thumbnail
@@ -0,0 +1,20 @@
return function()
local Thumbnail = require(script.Parent.Thumbnail)
it("should set fields without errors", function()
local testData =
{
targetId = 123456,
state = "Completed",
imageUrl = "a url",
}
local thumbnail = Thumbnail.fromThumbnailData(testData)
expect(thumbnail).to.be.a("table")
expect(thumbnail.universeId).to.equal("123456")
expect(thumbnail.state).to.equal("Completed")
expect(thumbnail.url).to.equal("a url")
end)
end
@@ -0,0 +1,18 @@
local ThumbnailRequest = {}
function ThumbnailRequest.new()
local self = {}
return self
end
function ThumbnailRequest.fromData(thumbnailType, thumbnailSize)
local self = ThumbnailRequest.new()
self.thumbnailType = thumbnailType
self.thumbnailSize = thumbnailSize
return self
end
return ThumbnailRequest
@@ -0,0 +1,134 @@
local CorePackages = game:GetService("CorePackages")
local Players = game:GetService("Players")
local MockId = require(CorePackages.AppTempCommon.LuaApp.MockId)
local User = {}
User.PresenceType = {
OFFLINE = "OFFLINE",
ONLINE = "ONLINE",
IN_GAME = "IN_GAME",
IN_STUDIO = "IN_STUDIO",
}
function User.new()
local self = {}
return self
end
function User.mock()
local self = User.new()
self.id = MockId()
self.isFetching = false
self.isFriend = false
self.lastLocation = nil
self.name = "USER NAME"
self.universeId = nil
self.placeId = nil
self.rootPlaceId = nil
self.gameInstanceId = nil
self.presence = User.PresenceType.OFFLINE
self.membership = nil
self.thumbnails = nil
self.lastOnline = nil
self.displayName = "DN+" .. self.name
return self
end
-- Note: Going forward, leverage User.fromDataTable() instead.
-- It accepts a more flexible parameter than User.fromData() and constructs the same User model
function User.fromData(id, name, isFriend)
local self = User.new()
self.id = tostring(id)
self.isFetching = false
self.isFriend = isFriend
self.lastLocation = nil
self.name = name
self.universeId = nil
self.placeId = nil
self.rootPlaceId = nil
self.gameInstanceId = nil
self.presence = (Players.LocalPlayer and self.id == tostring(Players.LocalPlayer.UserId))
and User.PresenceType.ONLINE or nil
self.thumbnails = nil
self.lastOnline = nil
return self
end
function User.fromDataTable(data)
local self = User.new()
self.id = tostring(data.id)
self.isFriend = data.isFriend
self.presence = (Players.LocalPlayer
and self.id == tostring(Players.LocalPlayer.UserId)) and User.PresenceType.ONLINE or nil
self.isFetching = false
self.lastLocation = nil
self.name = data.name
self.displayName = data.displayName or data.name
self.universeId = nil
self.placeId = nil
self.rootPlaceId = nil
self.gameInstanceId = nil
self.thumbnails = nil
self.lastOnline = nil
return self
end
function User.compare(user1, user2)
assert(not(user1 == nil and user2 == nil))
assert(user1 == nil or typeof(user1) == "table")
assert(user2 == nil or typeof(user2) == "table")
-- Return false if any of the provided input is nil(empty).
if not user1 or not user2 then
return false
end
for field, valueInUser2 in pairs(user2) do
if user1[field] ~= valueInUser2 then
return false
end
end
for field, valueInUser1 in pairs(user1) do
if user2[field] ~= valueInUser1 then
return false
end
end
return true
end
function User.userPresenceToText(localization, user)
local presence = user.presence
local lastLocation = user.lastLocation
if not presence then
return ''
end
if presence == User.PresenceType.OFFLINE then
return localization:Format("Common.Presence.Label.Offline")
elseif presence == User.PresenceType.ONLINE then
return localization:Format("Common.Presence.Label.Online")
elseif (presence == User.PresenceType.IN_GAME) or (presence == User.PresenceType.IN_STUDIO) then
if lastLocation ~= nil then
return lastLocation
else
return localization:Format("Common.Presence.Label.Online")
end
end
end
return User
@@ -0,0 +1,98 @@
return function()
local CorePackages = game:GetService("CorePackages")
local Immutable = require(CorePackages.AppTempCommon.Common.Immutable)
local User = require(CorePackages.AppTempCommon.LuaApp.Models.User)
it("should detect if provided users are identical", function()
local clone1 = User.fromData(1, "Andy", true)
local clone2 = Immutable.Set(clone1, "isFriend", true)
local result = User.compare(clone1, clone2)
expect(result).to.equal(true)
result = User.compare(clone2, clone1)
expect(result).to.equal(true)
end)
it("should detect when there is one or more fields with different values", function()
local andy = User.fromData(1, "Andy", true)
local ollie = Immutable.Set(andy, "name", "Ollie")
local result = User.compare(andy, ollie)
expect(result).to.equal(false)
result = User.compare(ollie, andy)
expect(result).to.equal(false)
end)
it("should detect descrepancy when one user model contains more fields than the other", function()
local andy = User.fromData(1, "Andy", true)
local secretlyNotAndy = Immutable.Set(andy, "someDifferentField", "I'm Ollie!")
local result = User.compare(andy, secretlyNotAndy)
expect(result).to.equal(false)
result = User.compare(secretlyNotAndy, andy)
expect(result).to.equal(false)
end)
it("should throw if invalid input is provided", function()
local aString = "I'm not a table."
local teddy = User.fromData(1, "Teddy", true)
expect(function() User.compare(nil, nil) end).to.throw()
expect(function() User.compare(aString, nil) end).to.throw()
expect(function() User.compare(nil, aString) end).to.throw()
expect(function() User.compare(aString, aString) end).to.throw()
expect(function() User.compare(teddy, aString) end).to.throw()
expect(function() User.compare(aString, teddy) end).to.throw()
end)
it("should return false if any one of the input is empty or nil)", function()
local emptyTable = {}
local teddy = User.fromData(1, "Teddy", true)
local result = User.compare(teddy, nil)
expect(result).to.equal(false)
result = User.compare(nil, teddy)
expect(result).to.equal(false)
result = User.compare(teddy, emptyTable)
expect(result).to.equal(false)
result = User.compare(emptyTable, teddy)
expect(result).to.equal(false)
end)
describe("fromDataTable", function()
it("should properly set user data", function()
local data = {
id = 1,
name = "FooBar",
displayName = "FooBar+DN",
isFriend = false,
}
local user = User.fromDataTable(data)
expect(user.id).to.equal("1")
expect(user.name).to.equal("FooBar")
expect(user.displayName).to.equal("FooBar+DN")
expect(user.isFriend).to.equal(false)
end)
it("should still set user data without a displayName property", function()
local data = {
id = 1,
name = "FooBar",
isFriend = false,
}
local user = User.fromDataTable(data)
expect(user.id).to.equal("1")
expect(user.name).to.equal("FooBar")
expect(user.displayName).to.equal("FooBar")
expect(user.isFriend).to.equal(false)
end)
end)
end
@@ -0,0 +1,51 @@
local percentReporting = tonumber(settings():GetFVariable("PercentReportingNetworkProfileAfterStartup"))
local FEATURE_NAME = "NetworkProfileDuringStartup"
local QUEUED_MEASURE_NAME = "Queued"
local NAME_LOOKUP_MEASURE_NAME = "NameLookup"
local CONNECT_MEASURE_NAME = "Connect"
local SSL_HANDSHAKE_MEASURE_NAME = "SSLHandshake"
local MAKE_REQUEST_MEASURE_NAME = "MakeRequest"
local RECEIVE_RESPONSE_MEASURE_NAME = "ReceiveResponse"
local NetworkProfiler = {}
NetworkProfiler.__index = NetworkProfiler
NetworkProfiler.aggregate = {
queued = 0.0,
nameLookup = 0.0,
connect = 0.0,
sslHandshake = 0.0,
makeRequest = 0.0,
receiveResponse = 0.0,
}
function NetworkProfiler:track(timeProfile)
self.aggregate.queued = self.aggregate.queued + timeProfile.queued
if timeProfile.nameLookup >= 0 then
self.aggregate.nameLookup = self.aggregate.nameLookup + timeProfile.nameLookup
end
if timeProfile.connect >= 0 then
self.aggregate.connect = self.aggregate.connect + timeProfile.connect
end
if timeProfile.sslHandshake >= 0 then
self.aggregate.sslHandshake = self.aggregate.sslHandshake + timeProfile.sslHandshake
end
if timeProfile.makeRequest >= 0 then
self.aggregate.makeRequest = self.aggregate.makeRequest + timeProfile.makeRequest
end
if timeProfile.receiveResponse >= 0 then
self.aggregate.receiveResponse = self.aggregate.receiveResponse + timeProfile.receiveResponse
end
end
function NetworkProfiler:report(reportToDiag)
reportToDiag(FEATURE_NAME, QUEUED_MEASURE_NAME, self.aggregate.queued, percentReporting)
reportToDiag(FEATURE_NAME, NAME_LOOKUP_MEASURE_NAME, self.aggregate.nameLookup, percentReporting)
reportToDiag(FEATURE_NAME, CONNECT_MEASURE_NAME, self.aggregate.connect, percentReporting)
reportToDiag(FEATURE_NAME, SSL_HANDSHAKE_MEASURE_NAME, self.aggregate.sslHandshake, percentReporting)
reportToDiag(FEATURE_NAME, MAKE_REQUEST_MEASURE_NAME, self.aggregate.makeRequest, percentReporting)
reportToDiag(FEATURE_NAME, RECEIVE_RESPONSE_MEASURE_NAME, self.aggregate.receiveResponse, percentReporting)
end
return NetworkProfiler
@@ -0,0 +1,8 @@
-----------------------------------------------------------------------------
--- ---
--- Under Migration to CorePackages ---
--- ---
-----------------------------------------------------------------------------
local CorePackages = game:GetService("CorePackages")
return require(CorePackages.Promise)
@@ -0,0 +1,91 @@
--[[
Provides utility functions for Promises
]]
local CorePackages = game:GetService("CorePackages")
local Result = require(CorePackages.AppTempCommon.LuaApp.Result)
local Promise = require(CorePackages.AppTempCommon.LuaApp.Promise)
local PromiseUtilities = {}
--[[
Accept a table of promises;
promises = {
[1] = Promise.resolve(),
["Home"] = Promise.reject(),
...
}
Returns a new promise that:
* is resolved when all input promises are finished.
returns the results of each individual promises in a list of Results
results = {
[1] = Result1,
["Home"] = Result2,
...
}
* is never rejected.
]]
function PromiseUtilities.Batch(promises)
assert(type(promises) == "table", "PromiseUtilities expects a list of Promises!")
local numberOfPromises = 0
for _, promise in pairs(promises) do
assert(Promise.is(promise), "PromiseUtilities expects a list of Promises!")
numberOfPromises = numberOfPromises + 1
end
return Promise.new(function(resolve, reject)
local totalCompleted = 0
local results = {}
local function promiseCompleted(key, success, value)
results[key] = Result.new(success, value)
totalCompleted = totalCompleted + 1
if totalCompleted == numberOfPromises then
resolve(results)
end
end
if next(promises) == nil then
resolve(results)
end
for key, promise in pairs(promises) do
promise:andThen(
function(result, ...)
if select("#", ...) > 0 then
warn("Promises in PromiseUtilities.Batch should not return tuple")
end
promiseCompleted(key, true, result)
end,
function(reason)
promiseCompleted(key, false, reason)
end
)
end
end)
end
function PromiseUtilities.CountResults(batchPromiseResults)
local totalCount = 0
local failureCount = 0
for _, result in pairs(batchPromiseResults) do
local success, _ = result:unwrap()
if not success then
failureCount = failureCount + 1
end
totalCount = totalCount + 1
end
return {
successCount = totalCount - failureCount,
failureCount = failureCount,
totalCount = totalCount,
}
end
return PromiseUtilities
@@ -0,0 +1,188 @@
return function()
local CorePackages = game:GetService("CorePackages")
local PromiseUtilities = require(CorePackages.AppTempCommon.LuaApp.PromiseUtilities)
local Promise = require(CorePackages.AppTempCommon.LuaApp.Promise)
local Result = require(CorePackages.AppTempCommon.LuaApp.Result)
local TableUtilities = require(CorePackages.AppTempCommon.LuaApp.TableUtilities)
describe("PromiseUtilities.Batch", function()
it("should assert if input is not a list of Promises", function()
expect(function()
PromiseUtilities.Batch()
end).to.throw()
expect(function()
PromiseUtilities.Batch(Promise.resolve(), Promise.resolve())
end).to.throw()
expect(function()
PromiseUtilities.Batch({
Promise.resolve(),
"something else"
})
end).to.throw()
end)
it("should invoke the given resolve callback when all promises are finished", function()
local promises = {
[1] = Promise.resolve(),
["Home"] = Promise.resolve()
}
local callCount = 0
local batchedPromise = PromiseUtilities.Batch(promises):andThen(
function()
callCount = callCount + 1
end
)
expect(batchedPromise).to.be.ok()
expect(callCount).to.equal(1)
expect(batchedPromise._status).to.equal(Promise.Status.Resolved)
end)
it("should not invoke any callbacks when one of the promises are not finished", function()
local promises = {
[1] = Promise.resolve(),
["Home"] = Promise.new(function() end)
}
local callCount = 0
local batchedPromise = PromiseUtilities.Batch(promises):andThen(
function()
callCount = callCount + 1
end,
function()
callCount = callCount + 1
end
)
expect(batchedPromise).to.be.ok()
expect(callCount).to.equal(0)
expect(batchedPromise._status).to.equal(Promise.Status.Started)
end)
it("should return the correct results of each individual promise", function()
local promises = {
[1] = Promise.resolve(5),
["Home"] = Promise.reject("failed")
}
local promiseResults = nil
local batchedPromise = PromiseUtilities.Batch(promises):andThen(
function(results)
promiseResults = results
end
)
expect(batchedPromise).to.be.ok()
expect(batchedPromise._status).to.equal(Promise.Status.Resolved)
expect(TableUtilities.FieldCount(promiseResults)).to.equal(2)
expect(Result.is(promiseResults[1])).to.equal(true)
local success1, value1 = promiseResults[1]:unwrap()
expect(success1).to.equal(true)
expect(value1).to.equal(5)
local isMatchCalled1 = false
promiseResults[1]:match(function(result)
expect(result).to.equal(5)
isMatchCalled1 = true
end,
function()
error("should not be called")
end)
expect(isMatchCalled1).to.equal(true)
expect(Result.is(promiseResults["Home"])).to.equal(true)
local success2, value2 = promiseResults["Home"]:unwrap()
expect(success2).to.equal(false)
expect(value2).to.equal("failed")
local isMatchCalled2 = false
promiseResults["Home"]:match(function()
error("should not be called")
end,
function(err)
expect(err).to.equal("failed")
isMatchCalled2 = true
end)
expect(isMatchCalled2).to.equal(true)
end)
it("should return the correct results of each individual promise that resolved later", function()
local resolveLater
local rejectLater
local promises = {
[1] = Promise.new(function(resolve)
resolveLater = resolve
end),
["Home"] = Promise.new(function(_, reject)
rejectLater = reject
end)
}
local promiseResults = nil
local batchedPromise = PromiseUtilities.Batch(promises):andThen(
function(results)
promiseResults = results
end
)
resolveLater(5)
rejectLater("failed")
expect(batchedPromise).to.be.ok()
expect(batchedPromise._status).to.equal(Promise.Status.Resolved)
expect(TableUtilities.FieldCount(promiseResults)).to.equal(2)
expect(Result.is(promiseResults[1])).to.equal(true)
local success1, value1 = promiseResults[1]:unwrap()
expect(success1).to.equal(true)
expect(value1).to.equal(5)
expect(Result.is(promiseResults["Home"])).to.equal(true)
local success2, value2 = promiseResults["Home"]:unwrap()
expect(success2).to.equal(false)
expect(value2).to.equal("failed")
end)
it("should resolve if given an empty list of promises", function()
local emptyPromises = {}
local callCount = 0
local batchedPromise = PromiseUtilities.Batch(emptyPromises):andThen(
function(results)
callCount = callCount + 1
end
)
expect(batchedPromise).to.be.ok()
expect(callCount).to.equal(1)
expect(batchedPromise._status).to.equal(Promise.Status.Resolved)
end)
end)
describe("PromiseUtilities.CountResults", function()
it("should count the results correctly", function()
local emptyResults = {}
local countResult = PromiseUtilities.CountResults(emptyResults)
expect(countResult).to.be.ok()
expect(countResult.successCount).to.equal(0)
expect(countResult.failureCount).to.equal(0)
expect(countResult.totalCount).to.equal(0)
local promiseResults = { Result.success(0), Result.success(0), Result.error(1) }
countResult = PromiseUtilities.CountResults(promiseResults)
expect(countResult).to.be.ok()
expect(countResult.successCount).to.equal(2)
expect(countResult.failureCount).to.equal(1)
expect(countResult.totalCount).to.equal(3)
end)
end)
end
@@ -0,0 +1,8 @@
{
"language": {
"mode": "nonstrict"
},
"lint": {
"ImplicitReturn": "fatal"
}
}
@@ -0,0 +1,29 @@
local CorePackages = game:GetService("CorePackages")
local UpdateFetchingStatus = require(CorePackages.AppTempCommon.LuaApp.Actions.UpdateFetchingStatus)
local Cryo = require(CorePackages.Cryo)
return function(state, action)
state = state or {}
if action.type == UpdateFetchingStatus.name then
local key = action.key
local status = action.status
local value
if status ~= nil then
value = status
else
value = Cryo.None
end
state = Cryo.Dictionary.join(
state,
{
[key] = value,
}
)
end
return state
end
@@ -0,0 +1,52 @@
return function()
local CorePackages = game:GetService("CorePackages")
local UpdateFetchingStatus = require(CorePackages.AppTempCommon.LuaApp.Actions.UpdateFetchingStatus)
local FetchingStatusReducer = require(CorePackages.AppTempCommon.LuaApp.Reducers.FetchingStatus)
local RetrievalStatus = require(CorePackages.AppTempCommon.LuaApp.Enum.RetrievalStatus)
local TableUtilities = require(CorePackages.AppTempCommon.LuaApp.TableUtilities)
local KEY_1 = "key_1"
local KEY_2 = "key_2"
describe("FetchingStatus", function()
it("should be empty by default", function()
local state = FetchingStatusReducer(nil, {})
expect(TableUtilities.FieldCount(state)).to.equal(0)
end)
it("should not be modified by other actions", function()
local oldState = FetchingStatusReducer(nil, {})
local newState = FetchingStatusReducer(oldState, { type = "not a real action" })
expect(newState).to.equal(oldState)
end)
it("should be changed using UpdateFetchingStatus", function()
local state = FetchingStatusReducer(nil, {})
state = FetchingStatusReducer(state, UpdateFetchingStatus(KEY_1, RetrievalStatus.Fetching))
expect(state[KEY_1]).to.equal(RetrievalStatus.Fetching)
state = FetchingStatusReducer(state, UpdateFetchingStatus(KEY_1, RetrievalStatus.Failed))
expect(state[KEY_1]).to.equal(RetrievalStatus.Failed)
end)
it("should store different values for different keys", function()
local state = FetchingStatusReducer(nil, {})
state = FetchingStatusReducer(state, UpdateFetchingStatus(KEY_1, RetrievalStatus.Failed))
state = FetchingStatusReducer(state, UpdateFetchingStatus(KEY_2, RetrievalStatus.Done))
expect(state[KEY_1]).to.equal(RetrievalStatus.Failed)
expect(state[KEY_2]).to.equal(RetrievalStatus.Done)
end)
it("should clear values for nil keys", function()
local state = { [KEY_1] = RetrievalStatus.Fetching }
state = FetchingStatusReducer(state, UpdateFetchingStatus(KEY_1, nil))
expect(state[KEY_1]).to.equal(nil)
end)
end)
end
@@ -0,0 +1,43 @@
local CorePackages = game:GetService("CorePackages")
local Immutable = require(CorePackages.AppTempCommon.Common.Immutable)
local RetrievalStatus = require(CorePackages.AppTempCommon.LuaApp.Enum.RetrievalStatus)
local FetchUserFriendsStarted = require(CorePackages.AppTempCommon.LuaApp.Actions.FetchUserFriendsStarted)
local FetchUserFriendsFailed = require(CorePackages.AppTempCommon.LuaApp.Actions.FetchUserFriendsFailed)
local FetchUserFriendsCompleted = require(CorePackages.AppTempCommon.LuaApp.Actions.FetchUserFriendsCompleted)
local function setFieldPerUser(state, fieldName, userId, value)
local field = state[fieldName] or {}
return Immutable.JoinDictionaries(state, {
[fieldName] = Immutable.JoinDictionaries(field, {
[userId] = value
})
})
end
local function setRetrievalStatus(state, userId, status)
return setFieldPerUser(state, "retrievalStatus", userId, status)
end
local function setRetrievalFailureResponse(state, userId, response)
return setFieldPerUser(state, "retrievalFailureResponse", userId, response)
end
return function(state, action)
state = state or {
retrievalStatus = {},
retrievalFailureResponse = {},
}
if action.type == FetchUserFriendsStarted.name then
state = setRetrievalStatus(state, action.userId, RetrievalStatus.Fetching)
elseif action.type == FetchUserFriendsFailed.name then
state = setRetrievalStatus(state, action.userId, RetrievalStatus.Failed)
state = setRetrievalFailureResponse(state, action.userId, action.response)
elseif action.type == FetchUserFriendsCompleted.name then
state = setRetrievalStatus(state, action.userId, RetrievalStatus.Done)
end
return state
end
@@ -0,0 +1,21 @@
local CorePackages = game:GetService("CorePackages")
local Immutable = require(CorePackages.AppTempCommon.Common.Immutable)
local ReceivedPlacesInfos = require(CorePackages.AppTempCommon.LuaApp.Actions.ReceivedPlacesInfos)
local LuaAppFlags = CorePackages.AppTempCommon.LuaApp.Flags
local convertUniverseIdToString = require(LuaAppFlags.ConvertUniverseIdToString)
return function(state, action)
state = state or {}
if action.type == ReceivedPlacesInfos.name then
for _, placeInfo in pairs(action.placesInfos) do
local universeId = convertUniverseIdToString(placeInfo.universeId)
state = Immutable.Set(state, universeId, placeInfo)
end
end
return state
end
@@ -0,0 +1,98 @@
local CorePackages = game:GetService("CorePackages")
local Immutable = require(CorePackages.AppTempCommon.Common.Immutable)
local AddUser = require(CorePackages.AppTempCommon.LuaApp.Actions.AddUser)
local AddUsers = require(CorePackages.AppTempCommon.LuaApp.Actions.AddUsers)
local ReceivedUserPresence = require(CorePackages.AppTempCommon.LuaChat.Actions.ReceivedUserPresence)
local RemoveUser = require(CorePackages.AppTempCommon.LuaApp.Actions.RemoveUser)
local SetUserIsFriend = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserIsFriend)
local SetUserMembershipType = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserMembershipType)
local SetUserPresence = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserPresence)
local SetUserThumbnail = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserThumbnail)
return function(state, action)
state = state or {}
if action.type == AddUser.name then
local user = action.user
state = Immutable.Set(state, user.id, user)
elseif action.type == AddUsers.name then
local addedUsers = action.users
local usersUpdate = {}
for userId, addedUser in pairs(addedUsers) do
local existingUser = state[userId]
if existingUser then
usersUpdate[userId] = Immutable.JoinDictionaries(existingUser, addedUser)
else
usersUpdate[userId] = addedUser
end
end
state = Immutable.JoinDictionaries(state, usersUpdate)
elseif action.type == SetUserIsFriend.name then
local user = state[action.userId]
if user then
local newUser = Immutable.Set(user, "isFriend", action.isFriend)
state = Immutable.Set(state, user.id, newUser)
else
warn("Setting isFriend on user", action.userId, "who doesn't exist yet")
end
elseif action.type == SetUserPresence.name then
local user = state[action.userId]
if user then
local newUser = Immutable.JoinDictionaries(user, {
presence = action.presence,
lastLocation = action.lastLocation,
})
state = Immutable.Set(state, user.id, newUser)
else
warn("Setting presence on user", action.userId, "who doesn't exist yet")
end
elseif action.type == ReceivedUserPresence.name then
local user = state[action.userId]
if user then
state = Immutable.JoinDictionaries(state, {
[action.userId] = Immutable.JoinDictionaries(user, {
presence = action.presence,
lastLocation = action.lastLocation,
placeId = action.placeId,
rootPlaceId = action.rootPlaceId,
gameInstanceId = action.gameInstanceId,
lastOnline = action.lastOnline,
universeId = action.universeId,
}),
})
end
elseif action.type == SetUserThumbnail.name then
local user = state[action.userId]
if user then
local thumbnails = user.thumbnails or {}
state = Immutable.JoinDictionaries(state, {
[action.userId] = Immutable.JoinDictionaries(user, {
thumbnails = Immutable.JoinDictionaries(thumbnails, {
[action.thumbnailType] = Immutable.JoinDictionaries(thumbnails[action.thumbnailType] or {}, {
[action.thumbnailSize] = action.image,
}),
}),
}),
})
end
elseif action.type == SetUserMembershipType.name then
local user = state[action.userId]
if user then
state = Immutable.JoinDictionaries(state, {
[action.userId] = Immutable.JoinDictionaries(user, {
membership = action.membershipType,
}),
})
end
elseif action.type == RemoveUser.name then
if state[action.userId] then
state = Immutable.RemoveFromDictionary(state, action.userId)
end
end
return state
end
@@ -0,0 +1,106 @@
return function()
local CorePackages = game:GetService("CorePackages")
local MockId = require(CorePackages.AppTempCommon.LuaApp.MockId)
local User = require(CorePackages.AppTempCommon.LuaApp.Models.User)
local Users = require(CorePackages.AppTempCommon.LuaApp.Reducers.Users)
local AddUser = require(CorePackages.AppTempCommon.LuaApp.Actions.AddUser)
local ReceivedUserPresence = require(CorePackages.AppTempCommon.LuaChat.Actions.ReceivedUserPresence)
local SetUserIsFriend = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserIsFriend)
local SetUserMembershipType = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserMembershipType)
local SetUserPresence = require(CorePackages.AppTempCommon.LuaApp.Actions.SetUserPresence)
describe("initial state", function()
it("should return an initial table when passed nil", function()
local state = Users(nil, {})
expect(state).to.be.a("table")
end)
end)
describe("AddUser", function()
it("should add a user to the store", function()
local user = User.mock()
local state = {}
state = Users(state, AddUser(user))
expect(state[user.id]).to.equal(user)
end)
end)
describe("SetUserIsFriend", function()
it("should set isFriend on an existing user", function()
local user = User.mock()
local state = {
[user.id] = user
}
expect(state[user.id].isFriend).to.equal(false)
state = Users(state, SetUserIsFriend(user.id, true))
expect(state[user.id].isFriend).to.equal(true)
state = Users(state, SetUserIsFriend(user.id, false))
expect(state[user.id].isFriend).to.equal(false)
end)
end)
describe("SetUserPresence", function()
it("should set presence on an existing user", function()
local user = User.mock()
local state = {
[user.id] = user
}
expect(state[user.id].presence).to.equal(User.PresenceType.OFFLINE)
state = Users(state, SetUserPresence(user.id, User.PresenceType.ONLINE))
expect(state[user.id].presence).to.equal(User.PresenceType.ONLINE)
state = Users(state, SetUserPresence(user.id, User.PresenceType.IN_GAME))
expect(state[user.id].presence).to.equal(User.PresenceType.IN_GAME)
state = Users(state, SetUserPresence(user.id, User.PresenceType.IN_STUDIO))
expect(state[user.id].presence).to.equal(User.PresenceType.IN_STUDIO)
end)
end)
describe("ReceivedUserPresence", function()
it("should set presence on an existing user", function()
local user = User.mock()
local state = {
[user.id] = user
}
local existingPresence = user.presence
local newPresence = 'ONLINE'
local lastLocation = MockId()
local newPlaceId = MockId()
state = Users(state, ReceivedUserPresence(user.id, newPresence, lastLocation, newPlaceId))
expect(user.presence).to.equal(existingPresence)
expect(state[user.id].presence).to.equal(newPresence)
expect(state[user.id].lastLocation).to.equal(lastLocation)
expect(state[user.id].placeId).to.equal(newPlaceId)
end)
end)
describe("SetUserMembershipType", function()
it("should set membership on an existing user", function()
local user = User.mock()
local state = {
[user.id] = user
}
local existingMembership = user.membership
local newMembership = Enum.MembershipType.BuildersClub
state = Users(state, SetUserMembershipType(user.id, newMembership))
expect(user.membership).to.equal(existingMembership)
expect(state[user.id].membership).to.equal(newMembership)
end)
end)
end
@@ -0,0 +1,8 @@
-----------------------------------------------------------------------------
--- ---
--- Under Migration to CorePackages ---
--- ---
-----------------------------------------------------------------------------
local CorePackages = game:GetService("CorePackages")
return require(CorePackages.Result)
@@ -0,0 +1,8 @@
{
"language": {
"mode": "nonstrict"
},
"lint": {
"ImplicitReturn": "fatal"
}
}

Some files were not shown because too many files have changed in this diff Show More