@quenty/promise 10.20.0 → 10.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +7 -0
- package/package.json +4 -4
- package/src/Shared/Promise.lua +18 -7
- package/src/Shared/Promise.spec.lua +447 -0
- package/src/Shared/PromiseRetryUtils.spec.lua +0 -3
- package/src/Shared/PromiseUtils.lua +17 -3
- package/src/Shared/PromiseUtils.spec.lua +275 -0
- package/src/Shared/Utility/PendingPromiseTracker.spec.lua +69 -0
- package/src/Shared/Utility/PromiseInstanceUtils.spec.lua +41 -0
- package/src/Shared/Utility/promiseChild.spec.lua +64 -0
- package/src/Shared/Utility/promisePropertyValue.spec.lua +53 -0
- package/src/Shared/Utility/promiseWait.spec.lua +26 -0
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,13 @@
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
|
|
5
5
|
|
|
6
|
+
# [10.21.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/promise@10.20.0...@quenty/promise@10.21.0) (2026-07-23)
|
|
7
|
+
|
|
8
|
+
### Features
|
|
9
|
+
|
|
10
|
+
- Add baseline player-mock and support across Nevermore for mocked players. ([567d121](https://github.com/Quenty/NevermoreEngine/commit/567d121ffc014b42391554088189a1a6296dda83))
|
|
11
|
+
- Add promise tests ([ff0a2a6](https://github.com/Quenty/NevermoreEngine/commit/ff0a2a6f72bcd63dbecf1af0a57870bac62c77ca))
|
|
12
|
+
|
|
6
13
|
# [10.20.0](https://github.com/Quenty/NevermoreEngine/compare/@quenty/promise@10.19.0...@quenty/promise@10.20.0) (2026-07-18)
|
|
7
14
|
|
|
8
15
|
**Note:** Version bump only for package @quenty/promise
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@quenty/promise",
|
|
3
|
-
"version": "10.
|
|
3
|
+
"version": "10.21.0",
|
|
4
4
|
"description": "Promise implementation for Roblox",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"Roblox",
|
|
@@ -30,14 +30,14 @@
|
|
|
30
30
|
"dependencies": {
|
|
31
31
|
"@quenty/deferred": "2.3.2",
|
|
32
32
|
"@quenty/loader": "10.11.0",
|
|
33
|
-
"@quenty/maid": "3.
|
|
33
|
+
"@quenty/maid": "3.11.0",
|
|
34
34
|
"@quenty/math": "2.7.5",
|
|
35
|
-
"@quenty/nevermore-test-runner": "1.
|
|
35
|
+
"@quenty/nevermore-test-runner": "1.5.0",
|
|
36
36
|
"@quenty/signal": "7.13.1",
|
|
37
37
|
"@quentystudios/jest-lua": "3.10.0-quenty.2"
|
|
38
38
|
},
|
|
39
39
|
"publishConfig": {
|
|
40
40
|
"access": "public"
|
|
41
41
|
},
|
|
42
|
-
"gitHead": "
|
|
42
|
+
"gitHead": "1de37218a2bedb8e3f8614a2e09bba9eddc812da"
|
|
43
43
|
}
|
package/src/Shared/Promise.lua
CHANGED
|
@@ -188,7 +188,8 @@ end
|
|
|
188
188
|
|
|
189
189
|
--[=[
|
|
190
190
|
Yields until the promise is complete, and errors if an error
|
|
191
|
-
exists, otherwise returns the fulfilled results.
|
|
191
|
+
exists, otherwise returns the fulfilled results. Raising the rejection to the caller consumes it —
|
|
192
|
+
no uncaught-exception warning fires for a rejection surfaced here.
|
|
192
193
|
|
|
193
194
|
@yields
|
|
194
195
|
@return T
|
|
@@ -197,6 +198,7 @@ function Promise.Wait<T...>(self: Promise<T...>): T...
|
|
|
197
198
|
if self._fulfilled then
|
|
198
199
|
return table.unpack(self._fulfilled, 1, self._fulfilled.n)
|
|
199
200
|
elseif self._rejected then
|
|
201
|
+
self._unconsumedException = false
|
|
200
202
|
error(tostring(self._rejected[1]), 2)
|
|
201
203
|
else
|
|
202
204
|
local waitingCoroutine = coroutine.running()
|
|
@@ -227,7 +229,8 @@ end
|
|
|
227
229
|
|
|
228
230
|
--[=[
|
|
229
231
|
Yields until the promise is complete, then returns a boolean indicating
|
|
230
|
-
the result, followed by the values from the promise.
|
|
232
|
+
the result, followed by the values from the promise. Reading the outcome consumes a rejection — the
|
|
233
|
+
caller has inspected it, so no uncaught-exception warning fires for it.
|
|
231
234
|
|
|
232
235
|
@yields
|
|
233
236
|
@return boolean, T
|
|
@@ -236,6 +239,7 @@ function Promise.Yield<T...>(self: Promise<T...>): (boolean, T...)
|
|
|
236
239
|
if self._fulfilled then
|
|
237
240
|
return true, table.unpack(self._fulfilled, 1, self._fulfilled.n)
|
|
238
241
|
elseif self._rejected then
|
|
242
|
+
self._unconsumedException = false
|
|
239
243
|
return false, table.unpack(self._rejected, 1, self._rejected.n)
|
|
240
244
|
else
|
|
241
245
|
local waitingCoroutine = coroutine.running()
|
|
@@ -346,7 +350,7 @@ function Promise.Reject<T...>(self: Promise<T...>, ...: any): ()
|
|
|
346
350
|
self:_reject(table.pack(...))
|
|
347
351
|
end
|
|
348
352
|
|
|
349
|
-
function Promise._reject<T...>(self: Promise<T...>, values: { any }): ()
|
|
353
|
+
function Promise._reject<T...>(self: Promise<T...>, values: { n: number, [number]: any }): ()
|
|
350
354
|
if not self._pendingExecuteList then
|
|
351
355
|
return
|
|
352
356
|
end
|
|
@@ -359,8 +363,11 @@ function Promise._reject<T...>(self: Promise<T...>, values: { any }): ()
|
|
|
359
363
|
self:_executeThen(unpack(data))
|
|
360
364
|
end
|
|
361
365
|
|
|
362
|
-
-- Check for uncaught exceptions
|
|
363
|
-
|
|
366
|
+
-- Check for uncaught exceptions. A single empty table carries no more information than an
|
|
367
|
+
-- empty rejection (aggregators may reject with an empty results table), so it is not
|
|
368
|
+
-- reported either.
|
|
369
|
+
local isEmptyTableRejection = values.n == 1 and type(values[1]) == "table" and next(values[1]) == nil
|
|
370
|
+
if self._unconsumedException and values.n > 0 and not isEmptyTableRejection then
|
|
364
371
|
task.defer(function()
|
|
365
372
|
-- Yield to end of frame, giving control back to Roblox.
|
|
366
373
|
-- This is the equivalent of giving something back to a task manager.
|
|
@@ -381,8 +388,10 @@ end
|
|
|
381
388
|
|
|
382
389
|
function Promise._toHumanReadable<T...>(_self: Promise<T...>, data: any): string
|
|
383
390
|
if type(data) == "table" then
|
|
391
|
+
-- A custom __tostring is the best human-readable form; only address-form tables
|
|
392
|
+
-- ("table: 0x...") fall through to JSON encoding.
|
|
384
393
|
local converted = tostring(data)
|
|
385
|
-
if string.sub(converted, 1,
|
|
394
|
+
if string.sub(converted, 1, 9) ~= "table: 0x" then
|
|
386
395
|
return converted
|
|
387
396
|
end
|
|
388
397
|
|
|
@@ -501,7 +510,8 @@ function Promise.Destroy<T...>(self: Promise<T...>): ()
|
|
|
501
510
|
end
|
|
502
511
|
|
|
503
512
|
--[=[
|
|
504
|
-
Returns the results from the promise.
|
|
513
|
+
Returns the results from the promise. Reading the outcome consumes a rejection — the caller has
|
|
514
|
+
inspected it, so no uncaught-exception warning fires for it.
|
|
505
515
|
|
|
506
516
|
:::warning
|
|
507
517
|
This API surface will error if the promise is still pending.
|
|
@@ -512,6 +522,7 @@ end
|
|
|
512
522
|
]=]
|
|
513
523
|
function Promise.GetResults<T...>(self: Promise<T...>): (boolean, T...)
|
|
514
524
|
if self._rejected then
|
|
525
|
+
self._unconsumedException = false
|
|
515
526
|
return false, table.unpack(self._rejected, 1, self._rejected.n)
|
|
516
527
|
elseif self._fulfilled then
|
|
517
528
|
return true, table.unpack(self._fulfilled, 1, self._fulfilled.n)
|
|
@@ -0,0 +1,447 @@
|
|
|
1
|
+
--!nonstrict
|
|
2
|
+
--[[
|
|
3
|
+
@class Promise.spec.lua
|
|
4
|
+
]]
|
|
5
|
+
|
|
6
|
+
local require = require(script.Parent.loader).load(script)
|
|
7
|
+
|
|
8
|
+
local Jest = require("Jest")
|
|
9
|
+
local Promise = require("Promise")
|
|
10
|
+
local PromiseTestUtils = require("PromiseTestUtils")
|
|
11
|
+
|
|
12
|
+
local describe = Jest.Globals.describe
|
|
13
|
+
local expect = Jest.Globals.expect
|
|
14
|
+
local it = Jest.Globals.it
|
|
15
|
+
|
|
16
|
+
describe("Promise.isPromise", function()
|
|
17
|
+
it("returns true for a promise", function()
|
|
18
|
+
expect(Promise.isPromise(Promise.new())).toEqual(true)
|
|
19
|
+
expect(Promise.isPromise(Promise.resolved(1))).toEqual(true)
|
|
20
|
+
end)
|
|
21
|
+
|
|
22
|
+
it("returns false for non-promise values", function()
|
|
23
|
+
expect(Promise.isPromise(nil)).toEqual(false)
|
|
24
|
+
expect(Promise.isPromise(5)).toEqual(false)
|
|
25
|
+
expect(Promise.isPromise("Promise")).toEqual(false)
|
|
26
|
+
expect(Promise.isPromise({})).toEqual(false)
|
|
27
|
+
expect(Promise.isPromise({ ClassName = "NotAPromise" })).toEqual(false)
|
|
28
|
+
end)
|
|
29
|
+
end)
|
|
30
|
+
|
|
31
|
+
describe("Promise.new", function()
|
|
32
|
+
it("starts pending with no executor", function()
|
|
33
|
+
local promise = Promise.new()
|
|
34
|
+
expect(promise:IsPending()).toEqual(true)
|
|
35
|
+
expect(promise:IsFulfilled()).toEqual(false)
|
|
36
|
+
expect(promise:IsRejected()).toEqual(false)
|
|
37
|
+
end)
|
|
38
|
+
|
|
39
|
+
it("resolves synchronously when the executor calls resolve", function()
|
|
40
|
+
local promise = Promise.new(function(resolve)
|
|
41
|
+
resolve("value")
|
|
42
|
+
end)
|
|
43
|
+
|
|
44
|
+
local outcome, value = PromiseTestUtils.awaitOutcome(promise)
|
|
45
|
+
expect(outcome).toEqual("resolved")
|
|
46
|
+
expect(value).toEqual("value")
|
|
47
|
+
end)
|
|
48
|
+
|
|
49
|
+
it("rejects when the executor calls reject", function()
|
|
50
|
+
local promise = Promise.new(function(_resolve, reject)
|
|
51
|
+
reject("boom")
|
|
52
|
+
end)
|
|
53
|
+
|
|
54
|
+
local outcome, err = PromiseTestUtils.awaitOutcome(promise)
|
|
55
|
+
expect(outcome).toEqual("rejected")
|
|
56
|
+
expect(err).toEqual("boom")
|
|
57
|
+
end)
|
|
58
|
+
end)
|
|
59
|
+
|
|
60
|
+
describe("Promise.spawn", function()
|
|
61
|
+
it("resolves from a spawned executor", function()
|
|
62
|
+
local promise = Promise.spawn(function(resolve)
|
|
63
|
+
resolve("spawned")
|
|
64
|
+
end)
|
|
65
|
+
|
|
66
|
+
local outcome, value = PromiseTestUtils.awaitOutcome(promise)
|
|
67
|
+
expect(outcome).toEqual("resolved")
|
|
68
|
+
expect(value).toEqual("spawned")
|
|
69
|
+
end)
|
|
70
|
+
end)
|
|
71
|
+
|
|
72
|
+
describe("Promise.delay", function()
|
|
73
|
+
it("resolves after the delay elapses", function()
|
|
74
|
+
local promise = Promise.delay(0, function(resolve)
|
|
75
|
+
resolve("delayed")
|
|
76
|
+
end)
|
|
77
|
+
|
|
78
|
+
local outcome, value = PromiseTestUtils.awaitOutcome(promise)
|
|
79
|
+
expect(outcome).toEqual("resolved")
|
|
80
|
+
expect(value).toEqual("delayed")
|
|
81
|
+
end)
|
|
82
|
+
|
|
83
|
+
it("is pending before the delay elapses", function()
|
|
84
|
+
local promise = Promise.delay(10, function(resolve)
|
|
85
|
+
resolve("late")
|
|
86
|
+
end)
|
|
87
|
+
expect(promise:IsPending()).toEqual(true)
|
|
88
|
+
end)
|
|
89
|
+
end)
|
|
90
|
+
|
|
91
|
+
describe("Promise.defer", function()
|
|
92
|
+
it("resolves from a deferred executor", function()
|
|
93
|
+
local promise = Promise.defer(function(resolve)
|
|
94
|
+
resolve("deferred")
|
|
95
|
+
end)
|
|
96
|
+
|
|
97
|
+
expect(promise:IsPending()).toEqual(true)
|
|
98
|
+
|
|
99
|
+
local outcome, value = PromiseTestUtils.awaitOutcome(promise)
|
|
100
|
+
expect(outcome).toEqual("resolved")
|
|
101
|
+
expect(value).toEqual("deferred")
|
|
102
|
+
end)
|
|
103
|
+
end)
|
|
104
|
+
|
|
105
|
+
describe("Promise.resolved", function()
|
|
106
|
+
it("creates an already-fulfilled promise", function()
|
|
107
|
+
local promise = Promise.resolved("done")
|
|
108
|
+
expect(promise:IsFulfilled()).toEqual(true)
|
|
109
|
+
expect(promise:IsPending()).toEqual(false)
|
|
110
|
+
|
|
111
|
+
local outcome, value = PromiseTestUtils.awaitOutcome(promise)
|
|
112
|
+
expect(outcome).toEqual("resolved")
|
|
113
|
+
expect(value).toEqual("done")
|
|
114
|
+
end)
|
|
115
|
+
|
|
116
|
+
it("reuses a single shared promise when resolving with no values", function()
|
|
117
|
+
expect(Promise.resolved()).toBe(Promise.resolved())
|
|
118
|
+
end)
|
|
119
|
+
|
|
120
|
+
it("returns the same promise when resolving an already-settled promise", function()
|
|
121
|
+
local inner = Promise.resolved("inner")
|
|
122
|
+
expect(Promise.resolved(inner)).toBe(inner)
|
|
123
|
+
end)
|
|
124
|
+
end)
|
|
125
|
+
|
|
126
|
+
describe("Promise.rejected", function()
|
|
127
|
+
it("creates an already-rejected promise", function()
|
|
128
|
+
local outcome, err = PromiseTestUtils.awaitOutcome(Promise.rejected("nope"))
|
|
129
|
+
expect(outcome).toEqual("rejected")
|
|
130
|
+
expect(err).toEqual("nope")
|
|
131
|
+
end)
|
|
132
|
+
|
|
133
|
+
it("reuses a single shared promise when rejecting with no values", function()
|
|
134
|
+
expect(Promise.rejected()).toBe(Promise.rejected())
|
|
135
|
+
end)
|
|
136
|
+
end)
|
|
137
|
+
|
|
138
|
+
describe("Promise:Resolve", function()
|
|
139
|
+
it("fulfills a pending promise", function()
|
|
140
|
+
local promise = Promise.new()
|
|
141
|
+
promise:Resolve(1, 2, 3)
|
|
142
|
+
|
|
143
|
+
local ok, a, b, c = promise:GetResults()
|
|
144
|
+
expect(ok).toEqual(true)
|
|
145
|
+
expect(a).toEqual(1)
|
|
146
|
+
expect(b).toEqual(2)
|
|
147
|
+
expect(c).toEqual(3)
|
|
148
|
+
end)
|
|
149
|
+
|
|
150
|
+
it("rejects with a TypeError when resolving to itself", function()
|
|
151
|
+
local promise = Promise.new()
|
|
152
|
+
promise:Resolve(promise)
|
|
153
|
+
|
|
154
|
+
local outcome, err = PromiseTestUtils.awaitOutcome(promise)
|
|
155
|
+
expect(outcome).toEqual("rejected")
|
|
156
|
+
expect(string.find(tostring(err), "Resolved to self", 1, true) ~= nil).toEqual(true)
|
|
157
|
+
end)
|
|
158
|
+
|
|
159
|
+
it("adopts the state of a resolved promise", function()
|
|
160
|
+
local promise = Promise.new()
|
|
161
|
+
promise:Resolve(Promise.resolved("adopted"))
|
|
162
|
+
|
|
163
|
+
local outcome, value = PromiseTestUtils.awaitOutcome(promise)
|
|
164
|
+
expect(outcome).toEqual("resolved")
|
|
165
|
+
expect(value).toEqual("adopted")
|
|
166
|
+
end)
|
|
167
|
+
|
|
168
|
+
it("adopts the rejection of a rejected promise", function()
|
|
169
|
+
local promise = Promise.new()
|
|
170
|
+
promise:Resolve(Promise.rejected("adopted rejection"))
|
|
171
|
+
|
|
172
|
+
local outcome, err = PromiseTestUtils.awaitOutcome(promise)
|
|
173
|
+
expect(outcome).toEqual("rejected")
|
|
174
|
+
expect(err).toEqual("adopted rejection")
|
|
175
|
+
end)
|
|
176
|
+
|
|
177
|
+
it("adopts the state of a pending promise once it settles", function()
|
|
178
|
+
local inner = Promise.new()
|
|
179
|
+
local promise = Promise.new()
|
|
180
|
+
promise:Resolve(inner)
|
|
181
|
+
|
|
182
|
+
expect(promise:IsPending()).toEqual(true)
|
|
183
|
+
|
|
184
|
+
inner:Resolve("later")
|
|
185
|
+
|
|
186
|
+
local outcome, value = PromiseTestUtils.awaitOutcome(promise)
|
|
187
|
+
expect(outcome).toEqual("resolved")
|
|
188
|
+
expect(value).toEqual("later")
|
|
189
|
+
end)
|
|
190
|
+
|
|
191
|
+
it("is a no-op once the promise has settled", function()
|
|
192
|
+
local promise = Promise.resolved("first")
|
|
193
|
+
promise:Resolve("second")
|
|
194
|
+
|
|
195
|
+
local _ok, value = promise:GetResults()
|
|
196
|
+
expect(value).toEqual("first")
|
|
197
|
+
end)
|
|
198
|
+
end)
|
|
199
|
+
|
|
200
|
+
describe("Promise:Reject", function()
|
|
201
|
+
it("rejects a pending promise", function()
|
|
202
|
+
local promise = Promise.new()
|
|
203
|
+
promise:Catch(function() end)
|
|
204
|
+
promise:Reject("rejected")
|
|
205
|
+
|
|
206
|
+
local ok, err = promise:GetResults()
|
|
207
|
+
expect(ok).toEqual(false)
|
|
208
|
+
expect(err).toEqual("rejected")
|
|
209
|
+
end)
|
|
210
|
+
|
|
211
|
+
it("is a no-op once the promise has settled", function()
|
|
212
|
+
local promise = Promise.resolved("value")
|
|
213
|
+
promise:Reject("too late")
|
|
214
|
+
expect(promise:IsFulfilled()).toEqual(true)
|
|
215
|
+
expect(promise:IsRejected()).toEqual(false)
|
|
216
|
+
end)
|
|
217
|
+
end)
|
|
218
|
+
|
|
219
|
+
describe("Promise:Wait", function()
|
|
220
|
+
it("returns the fulfilled values", function()
|
|
221
|
+
local a, b = Promise.resolved(1, 2):Wait()
|
|
222
|
+
expect(a).toEqual(1)
|
|
223
|
+
expect(b).toEqual(2)
|
|
224
|
+
end)
|
|
225
|
+
|
|
226
|
+
it("errors when the promise is rejected", function()
|
|
227
|
+
local promise = Promise.rejected("boom")
|
|
228
|
+
local ok, err = pcall(function()
|
|
229
|
+
promise:Wait()
|
|
230
|
+
end)
|
|
231
|
+
expect(ok).toEqual(false)
|
|
232
|
+
expect(string.find(tostring(err), "boom", 1, true) ~= nil).toEqual(true)
|
|
233
|
+
end)
|
|
234
|
+
|
|
235
|
+
it("yields until a pending promise resolves", function()
|
|
236
|
+
local promise = Promise.new()
|
|
237
|
+
task.defer(function()
|
|
238
|
+
promise:Resolve("eventually")
|
|
239
|
+
end)
|
|
240
|
+
expect(promise:Wait()).toEqual("eventually")
|
|
241
|
+
end)
|
|
242
|
+
end)
|
|
243
|
+
|
|
244
|
+
describe("Promise:Yield", function()
|
|
245
|
+
it("returns true and the values when fulfilled", function()
|
|
246
|
+
local ok, value = Promise.resolved("ok"):Yield()
|
|
247
|
+
expect(ok).toEqual(true)
|
|
248
|
+
expect(value).toEqual("ok")
|
|
249
|
+
end)
|
|
250
|
+
|
|
251
|
+
it("returns false and the error when rejected", function()
|
|
252
|
+
local promise = Promise.rejected("bad")
|
|
253
|
+
local ok, err = promise:Yield()
|
|
254
|
+
expect(ok).toEqual(false)
|
|
255
|
+
expect(err).toEqual("bad")
|
|
256
|
+
end)
|
|
257
|
+
|
|
258
|
+
it("yields until a pending promise settles", function()
|
|
259
|
+
local promise = Promise.new()
|
|
260
|
+
task.defer(function()
|
|
261
|
+
promise:Resolve("settled")
|
|
262
|
+
end)
|
|
263
|
+
local ok, value = promise:Yield()
|
|
264
|
+
expect(ok).toEqual(true)
|
|
265
|
+
expect(value).toEqual("settled")
|
|
266
|
+
end)
|
|
267
|
+
end)
|
|
268
|
+
|
|
269
|
+
describe("Promise:Then", function()
|
|
270
|
+
it("calls onFulfilled with the resolved value", function()
|
|
271
|
+
local outcome, value = PromiseTestUtils.awaitOutcome(Promise.resolved(10):Then(function(n)
|
|
272
|
+
return n * 2
|
|
273
|
+
end))
|
|
274
|
+
expect(outcome).toEqual("resolved")
|
|
275
|
+
expect(value).toEqual(20)
|
|
276
|
+
end)
|
|
277
|
+
|
|
278
|
+
it("calls onRejected with the rejection reason", function()
|
|
279
|
+
local outcome, value = PromiseTestUtils.awaitOutcome(Promise.rejected("err"):Then(nil, function(err)
|
|
280
|
+
return "recovered from " .. err
|
|
281
|
+
end))
|
|
282
|
+
expect(outcome).toEqual("resolved")
|
|
283
|
+
expect(value).toEqual("recovered from err")
|
|
284
|
+
end)
|
|
285
|
+
|
|
286
|
+
it("propagates fulfillment when onFulfilled is omitted", function()
|
|
287
|
+
local outcome, value = PromiseTestUtils.awaitOutcome(Promise.resolved("passthrough"):Then(nil, function() end))
|
|
288
|
+
expect(outcome).toEqual("resolved")
|
|
289
|
+
expect(value).toEqual("passthrough")
|
|
290
|
+
end)
|
|
291
|
+
|
|
292
|
+
it("propagates rejection when onRejected is omitted", function()
|
|
293
|
+
local source = Promise.rejected("still bad")
|
|
294
|
+
-- Then with no onRejected does not consume the source rejection, so handle it separately to
|
|
295
|
+
-- avoid an uncaught-exception warning.
|
|
296
|
+
source:Catch(function() end)
|
|
297
|
+
|
|
298
|
+
local outcome, err = PromiseTestUtils.awaitOutcome(source:Then(function() end))
|
|
299
|
+
expect(outcome).toEqual("rejected")
|
|
300
|
+
expect(err).toEqual("still bad")
|
|
301
|
+
end)
|
|
302
|
+
|
|
303
|
+
it("runs handlers attached while pending", function()
|
|
304
|
+
local promise = Promise.new()
|
|
305
|
+
local chained = promise:Then(function(n)
|
|
306
|
+
return n + 1
|
|
307
|
+
end)
|
|
308
|
+
|
|
309
|
+
task.defer(function()
|
|
310
|
+
promise:Resolve(41)
|
|
311
|
+
end)
|
|
312
|
+
|
|
313
|
+
local outcome, value = PromiseTestUtils.awaitOutcome(chained)
|
|
314
|
+
expect(outcome).toEqual("resolved")
|
|
315
|
+
expect(value).toEqual(42)
|
|
316
|
+
end)
|
|
317
|
+
|
|
318
|
+
it("chains when the handler returns a promise", function()
|
|
319
|
+
local outcome, value = PromiseTestUtils.awaitOutcome(Promise.resolved(1):Then(function()
|
|
320
|
+
return Promise.resolved("from inner promise")
|
|
321
|
+
end))
|
|
322
|
+
expect(outcome).toEqual("resolved")
|
|
323
|
+
expect(value).toEqual("from inner promise")
|
|
324
|
+
end)
|
|
325
|
+
end)
|
|
326
|
+
|
|
327
|
+
describe("Promise:Catch", function()
|
|
328
|
+
it("handles a rejection", function()
|
|
329
|
+
local outcome, value = PromiseTestUtils.awaitOutcome(Promise.rejected("caught"):Catch(function(err)
|
|
330
|
+
return "handled " .. err
|
|
331
|
+
end))
|
|
332
|
+
expect(outcome).toEqual("resolved")
|
|
333
|
+
expect(value).toEqual("handled caught")
|
|
334
|
+
end)
|
|
335
|
+
|
|
336
|
+
it("does not fire for a fulfilled promise", function()
|
|
337
|
+
local called = { value = false }
|
|
338
|
+
local outcome, value = PromiseTestUtils.awaitOutcome(Promise.resolved("fine"):Catch(function()
|
|
339
|
+
called.value = true
|
|
340
|
+
end))
|
|
341
|
+
expect(outcome).toEqual("resolved")
|
|
342
|
+
expect(value).toEqual("fine")
|
|
343
|
+
expect(called.value).toEqual(false)
|
|
344
|
+
end)
|
|
345
|
+
end)
|
|
346
|
+
|
|
347
|
+
describe("Promise:Tap", function()
|
|
348
|
+
it("passes the original value through regardless of the handler return", function()
|
|
349
|
+
local seen = {}
|
|
350
|
+
local outcome, value = PromiseTestUtils.awaitOutcome(Promise.resolved("original"):Tap(function(v)
|
|
351
|
+
seen.value = v
|
|
352
|
+
return "ignored"
|
|
353
|
+
end))
|
|
354
|
+
expect(outcome).toEqual("resolved")
|
|
355
|
+
expect(value).toEqual("original")
|
|
356
|
+
expect(seen.value).toEqual("original")
|
|
357
|
+
end)
|
|
358
|
+
end)
|
|
359
|
+
|
|
360
|
+
describe("Promise:Finally", function()
|
|
361
|
+
it("runs on fulfillment", function()
|
|
362
|
+
local ran = { value = false }
|
|
363
|
+
Promise.resolved("x"):Finally(function()
|
|
364
|
+
ran.value = true
|
|
365
|
+
end)
|
|
366
|
+
expect(ran.value).toEqual(true)
|
|
367
|
+
end)
|
|
368
|
+
|
|
369
|
+
it("runs on rejection", function()
|
|
370
|
+
local ran = { value = false }
|
|
371
|
+
Promise.rejected("x"):Finally(function()
|
|
372
|
+
ran.value = true
|
|
373
|
+
end)
|
|
374
|
+
expect(ran.value).toEqual(true)
|
|
375
|
+
end)
|
|
376
|
+
end)
|
|
377
|
+
|
|
378
|
+
describe("Promise:Destroy", function()
|
|
379
|
+
it("rejects the promise", function()
|
|
380
|
+
local promise = Promise.new()
|
|
381
|
+
promise:Catch(function() end)
|
|
382
|
+
promise:Destroy()
|
|
383
|
+
expect(promise:IsRejected()).toEqual(true)
|
|
384
|
+
end)
|
|
385
|
+
end)
|
|
386
|
+
|
|
387
|
+
describe("Promise:GetResults", function()
|
|
388
|
+
it("errors while pending", function()
|
|
389
|
+
local ok, err = pcall(function()
|
|
390
|
+
Promise.new():GetResults()
|
|
391
|
+
end)
|
|
392
|
+
expect(ok).toEqual(false)
|
|
393
|
+
expect(string.find(tostring(err), "pending", 1, true) ~= nil).toEqual(true)
|
|
394
|
+
end)
|
|
395
|
+
|
|
396
|
+
it("returns true and the values when fulfilled", function()
|
|
397
|
+
local ok, a, b = Promise.resolved("a", "b"):GetResults()
|
|
398
|
+
expect(ok).toEqual(true)
|
|
399
|
+
expect(a).toEqual("a")
|
|
400
|
+
expect(b).toEqual("b")
|
|
401
|
+
end)
|
|
402
|
+
|
|
403
|
+
it("returns false and the error when rejected", function()
|
|
404
|
+
local promise = Promise.rejected("failure")
|
|
405
|
+
local ok, err = promise:GetResults()
|
|
406
|
+
expect(ok).toEqual(false)
|
|
407
|
+
expect(err).toEqual("failure")
|
|
408
|
+
end)
|
|
409
|
+
|
|
410
|
+
it("consumes the rejection", function()
|
|
411
|
+
local promise = Promise.rejected("failure")
|
|
412
|
+
promise:GetResults()
|
|
413
|
+
expect((promise :: any)._unconsumedException).toEqual(false)
|
|
414
|
+
end)
|
|
415
|
+
end)
|
|
416
|
+
|
|
417
|
+
describe("Promise._toHumanReadable", function()
|
|
418
|
+
local promise = Promise.new()
|
|
419
|
+
|
|
420
|
+
it("stringifies non-table values", function()
|
|
421
|
+
expect(promise:_toHumanReadable("failure")).toEqual("failure")
|
|
422
|
+
expect(promise:_toHumanReadable(5)).toEqual("5")
|
|
423
|
+
expect(promise:_toHumanReadable(nil)).toEqual("nil")
|
|
424
|
+
expect(promise:_toHumanReadable(false)).toEqual("false")
|
|
425
|
+
end)
|
|
426
|
+
|
|
427
|
+
it("uses a table's custom __tostring when present", function()
|
|
428
|
+
local data = setmetatable({}, {
|
|
429
|
+
__tostring = function()
|
|
430
|
+
return "CustomError<oops>"
|
|
431
|
+
end,
|
|
432
|
+
})
|
|
433
|
+
expect(promise:_toHumanReadable(data)).toEqual("CustomError<oops>")
|
|
434
|
+
end)
|
|
435
|
+
|
|
436
|
+
it("JSON-encodes a plain table instead of returning its address", function()
|
|
437
|
+
expect(promise:_toHumanReadable({ code = 500 })).toEqual('{"code":500}')
|
|
438
|
+
expect(promise:_toHumanReadable({})).toEqual("[]")
|
|
439
|
+
end)
|
|
440
|
+
|
|
441
|
+
it("falls back to tostring when the table cannot be JSON-encoded", function()
|
|
442
|
+
local data = {}
|
|
443
|
+
data.cycle = data
|
|
444
|
+
local result = promise:_toHumanReadable(data)
|
|
445
|
+
expect(string.sub(result, 1, 9)).toEqual("table: 0x")
|
|
446
|
+
end)
|
|
447
|
+
end)
|
|
@@ -20,8 +20,6 @@ local FAST_OPTIONS = {
|
|
|
20
20
|
printWarning = false,
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
-- Returns a callback that rejects with `errorMessage` for its first `failureCount` attempts, then
|
|
24
|
-
-- resolves with `value`, and a counter table exposing how many attempts were made.
|
|
25
23
|
local function newFlakyCallback(failureCount: number, errorMessage: string, value: any)
|
|
26
24
|
local attempts = { count = 0 }
|
|
27
25
|
local function callback()
|
|
@@ -103,7 +101,6 @@ describe("PromiseRetryUtils.retry", function()
|
|
|
103
101
|
|
|
104
102
|
expect(outcome).toEqual("rejected")
|
|
105
103
|
expect(attempts.count).toEqual(3)
|
|
106
|
-
-- Consulted after every failure, including the final attempt.
|
|
107
104
|
expect(consulted.count).toEqual(3)
|
|
108
105
|
end)
|
|
109
106
|
end)
|
|
@@ -145,6 +145,11 @@ end
|
|
|
145
145
|
--[=[
|
|
146
146
|
Combines the result of promises together
|
|
147
147
|
|
|
148
|
+
On failure, rejects with the results table when any input rejected with a value (the
|
|
149
|
+
error is stored under that input's key). When every failing input rejected with no
|
|
150
|
+
values (i.e. was cancelled), rejects with no values so the cancellation stays a
|
|
151
|
+
cancellation instead of becoming a valued rejection of partial results.
|
|
152
|
+
|
|
148
153
|
@param stateTable any
|
|
149
154
|
@return Promise<any>
|
|
150
155
|
]=]
|
|
@@ -168,16 +173,25 @@ function PromiseUtils.combine(stateTable: any): Promise.Promise<any>
|
|
|
168
173
|
|
|
169
174
|
local returnPromise = Promise.new()
|
|
170
175
|
local allFulfilled = true
|
|
176
|
+
local sawRejectionValue = false
|
|
171
177
|
|
|
172
178
|
local function syncronize(key, isFullfilled)
|
|
173
|
-
return function(value)
|
|
179
|
+
return function(value, ...)
|
|
174
180
|
allFulfilled = allFulfilled and isFullfilled
|
|
181
|
+
if not isFullfilled and (value ~= nil or select("#", ...) > 0) then
|
|
182
|
+
sawRejectionValue = true
|
|
183
|
+
end
|
|
175
184
|
results[key] = value
|
|
176
185
|
remainingCount = remainingCount - 1
|
|
177
186
|
|
|
178
187
|
if remainingCount == 0 then
|
|
179
|
-
|
|
180
|
-
|
|
188
|
+
if allFulfilled then
|
|
189
|
+
returnPromise:Resolve(results)
|
|
190
|
+
elseif sawRejectionValue then
|
|
191
|
+
returnPromise:Reject(results)
|
|
192
|
+
else
|
|
193
|
+
returnPromise:Reject()
|
|
194
|
+
end
|
|
181
195
|
end
|
|
182
196
|
end
|
|
183
197
|
end
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
--!nonstrict
|
|
2
|
+
--[[
|
|
3
|
+
@class PromiseUtils.spec.lua
|
|
4
|
+
]]
|
|
5
|
+
|
|
6
|
+
local require = require(script.Parent.loader).load(script)
|
|
7
|
+
|
|
8
|
+
local Jest = require("Jest")
|
|
9
|
+
local Promise = require("Promise")
|
|
10
|
+
local PromiseTestUtils = require("PromiseTestUtils")
|
|
11
|
+
local PromiseUtils = require("PromiseUtils")
|
|
12
|
+
local Signal = require("Signal")
|
|
13
|
+
|
|
14
|
+
local describe = Jest.Globals.describe
|
|
15
|
+
local expect = Jest.Globals.expect
|
|
16
|
+
local it = Jest.Globals.it
|
|
17
|
+
|
|
18
|
+
describe("PromiseUtils.any / race", function()
|
|
19
|
+
it("is aliased as race", function()
|
|
20
|
+
expect(PromiseUtils.race).toBe(PromiseUtils.any)
|
|
21
|
+
end)
|
|
22
|
+
|
|
23
|
+
it("resolves with the first promise to resolve", function()
|
|
24
|
+
local a = Promise.new()
|
|
25
|
+
local b = Promise.resolved("b wins")
|
|
26
|
+
|
|
27
|
+
local outcome, value = PromiseTestUtils.awaitOutcome(PromiseUtils.any({ a, b }))
|
|
28
|
+
expect(outcome).toEqual("resolved")
|
|
29
|
+
expect(value).toEqual("b wins")
|
|
30
|
+
|
|
31
|
+
a:Resolve("ignored")
|
|
32
|
+
end)
|
|
33
|
+
|
|
34
|
+
it("rejects if the first to settle rejects", function()
|
|
35
|
+
local outcome, err =
|
|
36
|
+
PromiseTestUtils.awaitOutcome(PromiseUtils.any({ Promise.new(), Promise.rejected("boom") }))
|
|
37
|
+
expect(outcome).toEqual("rejected")
|
|
38
|
+
expect(err).toEqual("boom")
|
|
39
|
+
end)
|
|
40
|
+
end)
|
|
41
|
+
|
|
42
|
+
describe("PromiseUtils.delayed", function()
|
|
43
|
+
it("resolves after the delay", function()
|
|
44
|
+
local outcome = PromiseTestUtils.awaitOutcome(PromiseUtils.delayed(0))
|
|
45
|
+
expect(outcome).toEqual("resolved")
|
|
46
|
+
end)
|
|
47
|
+
|
|
48
|
+
it("is pending before the delay elapses", function()
|
|
49
|
+
local promise = PromiseUtils.delayed(10)
|
|
50
|
+
expect(promise:IsPending()).toEqual(true)
|
|
51
|
+
end)
|
|
52
|
+
end)
|
|
53
|
+
|
|
54
|
+
describe("PromiseUtils.all", function()
|
|
55
|
+
it("resolves an empty list immediately", function()
|
|
56
|
+
expect(PromiseUtils.all({}):IsFulfilled()).toEqual(true)
|
|
57
|
+
end)
|
|
58
|
+
|
|
59
|
+
it("returns the same promise for a single-element list", function()
|
|
60
|
+
local only = Promise.resolved("only")
|
|
61
|
+
expect(PromiseUtils.all({ only })).toBe(only)
|
|
62
|
+
end)
|
|
63
|
+
|
|
64
|
+
it("resolves with every value once all resolve", function()
|
|
65
|
+
local combined = PromiseUtils.all({ Promise.resolved("a"), Promise.resolved("b"), Promise.resolved("c") })
|
|
66
|
+
|
|
67
|
+
expect(PromiseTestUtils.awaitSettled(combined)).toEqual(true)
|
|
68
|
+
local ok, a, b, c = combined:GetResults()
|
|
69
|
+
expect(ok).toEqual(true)
|
|
70
|
+
expect(a).toEqual("a")
|
|
71
|
+
expect(b).toEqual("b")
|
|
72
|
+
expect(c).toEqual("c")
|
|
73
|
+
end)
|
|
74
|
+
|
|
75
|
+
it("rejects if any promise rejects", function()
|
|
76
|
+
-- all() rejects with the positional results array (resolved values and rejection reasons mixed
|
|
77
|
+
-- by index), so only the rejected outcome itself is asserted here.
|
|
78
|
+
local outcome =
|
|
79
|
+
PromiseTestUtils.awaitOutcome(PromiseUtils.all({ Promise.resolved("a"), Promise.rejected("nope") }))
|
|
80
|
+
expect(outcome).toEqual("rejected")
|
|
81
|
+
end)
|
|
82
|
+
|
|
83
|
+
it("waits for pending promises before resolving", function()
|
|
84
|
+
local pending = Promise.new()
|
|
85
|
+
local combined = PromiseUtils.all({ Promise.resolved("a"), pending })
|
|
86
|
+
expect(combined:IsPending()).toEqual(true)
|
|
87
|
+
|
|
88
|
+
pending:Resolve("b")
|
|
89
|
+
expect(PromiseTestUtils.awaitSettled(combined)).toEqual(true)
|
|
90
|
+
local ok, a, b = combined:GetResults()
|
|
91
|
+
expect(ok).toEqual(true)
|
|
92
|
+
expect(a).toEqual("a")
|
|
93
|
+
expect(b).toEqual("b")
|
|
94
|
+
end)
|
|
95
|
+
end)
|
|
96
|
+
|
|
97
|
+
describe("PromiseUtils.firstSuccessOrLastFailure", function()
|
|
98
|
+
it("resolves with the first success", function()
|
|
99
|
+
local outcome, value = PromiseTestUtils.awaitOutcome(
|
|
100
|
+
PromiseUtils.firstSuccessOrLastFailure({ Promise.rejected("bad"), Promise.resolved("good") })
|
|
101
|
+
)
|
|
102
|
+
expect(outcome).toEqual("resolved")
|
|
103
|
+
expect(value).toEqual("good")
|
|
104
|
+
end)
|
|
105
|
+
|
|
106
|
+
it("rejects with the last failure when all fail", function()
|
|
107
|
+
local outcome, err = PromiseTestUtils.awaitOutcome(
|
|
108
|
+
PromiseUtils.firstSuccessOrLastFailure({ Promise.rejected("first"), Promise.rejected("last") })
|
|
109
|
+
)
|
|
110
|
+
expect(outcome).toEqual("rejected")
|
|
111
|
+
expect(err).toEqual("last")
|
|
112
|
+
end)
|
|
113
|
+
|
|
114
|
+
it("returns the same promise for a single-element list", function()
|
|
115
|
+
local only = Promise.resolved("only")
|
|
116
|
+
expect(PromiseUtils.firstSuccessOrLastFailure({ only })).toBe(only)
|
|
117
|
+
end)
|
|
118
|
+
end)
|
|
119
|
+
|
|
120
|
+
describe("PromiseUtils.combine", function()
|
|
121
|
+
it("passes through a table with no promises", function()
|
|
122
|
+
local outcome, value = PromiseTestUtils.awaitOutcome(PromiseUtils.combine({ a = 1, b = 2 }))
|
|
123
|
+
expect(outcome).toEqual("resolved")
|
|
124
|
+
expect(value.a).toEqual(1)
|
|
125
|
+
expect(value.b).toEqual(2)
|
|
126
|
+
end)
|
|
127
|
+
|
|
128
|
+
it("resolves with a table mixing promises and plain values", function()
|
|
129
|
+
local combined = PromiseUtils.combine({ a = Promise.resolved("A"), b = "B" })
|
|
130
|
+
|
|
131
|
+
expect(PromiseTestUtils.awaitSettled(combined)).toEqual(true)
|
|
132
|
+
local ok, results = combined:GetResults()
|
|
133
|
+
expect(ok).toEqual(true)
|
|
134
|
+
expect(results.a).toEqual("A")
|
|
135
|
+
expect(results.b).toEqual("B")
|
|
136
|
+
end)
|
|
137
|
+
|
|
138
|
+
it("rejects if any promise rejects", function()
|
|
139
|
+
local outcome = PromiseTestUtils.awaitOutcome(PromiseUtils.combine({ a = Promise.rejected("bad"), b = "B" }))
|
|
140
|
+
expect(outcome).toEqual("rejected")
|
|
141
|
+
end)
|
|
142
|
+
|
|
143
|
+
it("rejects with the results table carrying the error under its key", function()
|
|
144
|
+
local combined = PromiseUtils.combine({
|
|
145
|
+
a = Promise.rejected("bad"),
|
|
146
|
+
b = Promise.resolved("B"),
|
|
147
|
+
})
|
|
148
|
+
combined:Catch(function() end)
|
|
149
|
+
|
|
150
|
+
expect(PromiseTestUtils.awaitSettled(combined)).toEqual(true)
|
|
151
|
+
local ok, results = combined:GetResults()
|
|
152
|
+
expect(ok).toEqual(false)
|
|
153
|
+
expect(results.a).toEqual("bad")
|
|
154
|
+
expect(results.b).toEqual("B")
|
|
155
|
+
end)
|
|
156
|
+
|
|
157
|
+
it("rejects with no values when the only failure is a cancellation", function()
|
|
158
|
+
local pending = Promise.new()
|
|
159
|
+
local combined = PromiseUtils.combine({
|
|
160
|
+
a = pending,
|
|
161
|
+
b = Promise.resolved("B"),
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
local rejectionCount = nil
|
|
165
|
+
combined:Then(nil, function(...)
|
|
166
|
+
rejectionCount = select("#", ...)
|
|
167
|
+
end)
|
|
168
|
+
|
|
169
|
+
pending:Reject() -- cancellation: rejection with no values
|
|
170
|
+
|
|
171
|
+
expect(PromiseTestUtils.awaitSettled(combined)).toEqual(true)
|
|
172
|
+
expect(combined:IsRejected()).toEqual(true)
|
|
173
|
+
expect(rejectionCount).toEqual(0)
|
|
174
|
+
end)
|
|
175
|
+
|
|
176
|
+
it("still rejects with the results table when a cancellation and a real error mix", function()
|
|
177
|
+
local cancelled = Promise.new()
|
|
178
|
+
local combined = PromiseUtils.combine({
|
|
179
|
+
a = cancelled,
|
|
180
|
+
b = Promise.rejected("bad"),
|
|
181
|
+
c = Promise.resolved("C"),
|
|
182
|
+
})
|
|
183
|
+
combined:Catch(function() end)
|
|
184
|
+
|
|
185
|
+
cancelled:Reject()
|
|
186
|
+
|
|
187
|
+
expect(PromiseTestUtils.awaitSettled(combined)).toEqual(true)
|
|
188
|
+
local ok, results = combined:GetResults()
|
|
189
|
+
expect(ok).toEqual(false)
|
|
190
|
+
expect(results.a).toEqual(nil)
|
|
191
|
+
expect(results.b).toEqual("bad")
|
|
192
|
+
expect(results.c).toEqual("C")
|
|
193
|
+
end)
|
|
194
|
+
end)
|
|
195
|
+
|
|
196
|
+
describe("PromiseUtils.invert", function()
|
|
197
|
+
it("turns a resolved promise into a rejection", function()
|
|
198
|
+
local outcome, err = PromiseTestUtils.awaitOutcome(PromiseUtils.invert(Promise.resolved("was ok")))
|
|
199
|
+
expect(outcome).toEqual("rejected")
|
|
200
|
+
expect(err).toEqual("was ok")
|
|
201
|
+
end)
|
|
202
|
+
|
|
203
|
+
it("turns a rejected promise into a resolution", function()
|
|
204
|
+
local source = Promise.rejected("was bad")
|
|
205
|
+
|
|
206
|
+
local outcome, value = PromiseTestUtils.awaitOutcome(PromiseUtils.invert(source))
|
|
207
|
+
expect(outcome).toEqual("resolved")
|
|
208
|
+
expect(value).toEqual("was bad")
|
|
209
|
+
end)
|
|
210
|
+
|
|
211
|
+
it("inverts a pending promise once it settles", function()
|
|
212
|
+
local source = Promise.new()
|
|
213
|
+
local inverted = PromiseUtils.invert(source)
|
|
214
|
+
expect(inverted:IsPending()).toEqual(true)
|
|
215
|
+
|
|
216
|
+
source:Resolve("later")
|
|
217
|
+
local outcome, err = PromiseTestUtils.awaitOutcome(inverted)
|
|
218
|
+
expect(outcome).toEqual("rejected")
|
|
219
|
+
expect(err).toEqual("later")
|
|
220
|
+
end)
|
|
221
|
+
end)
|
|
222
|
+
|
|
223
|
+
describe("PromiseUtils.fromSignal", function()
|
|
224
|
+
it("resolves when the signal fires", function()
|
|
225
|
+
local signal = Signal.new()
|
|
226
|
+
local promise = PromiseUtils.fromSignal(signal)
|
|
227
|
+
expect(promise:IsPending()).toEqual(true)
|
|
228
|
+
|
|
229
|
+
signal:Fire("fired")
|
|
230
|
+
|
|
231
|
+
local outcome, value = PromiseTestUtils.awaitOutcome(promise)
|
|
232
|
+
expect(outcome).toEqual("resolved")
|
|
233
|
+
expect(value).toEqual("fired")
|
|
234
|
+
|
|
235
|
+
signal:Destroy()
|
|
236
|
+
end)
|
|
237
|
+
|
|
238
|
+
it("disconnects the signal once settled", function()
|
|
239
|
+
local signal = Signal.new()
|
|
240
|
+
local promise = PromiseUtils.fromSignal(signal)
|
|
241
|
+
|
|
242
|
+
signal:Fire("first")
|
|
243
|
+
PromiseTestUtils.awaitSettled(promise)
|
|
244
|
+
|
|
245
|
+
-- A second fire must not change the already-resolved value.
|
|
246
|
+
signal:Fire("second")
|
|
247
|
+
local _ok, value = promise:GetResults()
|
|
248
|
+
expect(value).toEqual("first")
|
|
249
|
+
|
|
250
|
+
signal:Destroy()
|
|
251
|
+
end)
|
|
252
|
+
end)
|
|
253
|
+
|
|
254
|
+
describe("PromiseUtils.timeout", function()
|
|
255
|
+
it("returns the same promise when it is already settled", function()
|
|
256
|
+
local settled = Promise.resolved("done")
|
|
257
|
+
expect(PromiseUtils.timeout(1, settled)).toBe(settled)
|
|
258
|
+
end)
|
|
259
|
+
|
|
260
|
+
it("rejects once the timeout elapses", function()
|
|
261
|
+
local outcome = PromiseTestUtils.awaitOutcome(PromiseUtils.timeout(0, Promise.new()))
|
|
262
|
+
expect(outcome).toEqual("rejected")
|
|
263
|
+
end)
|
|
264
|
+
|
|
265
|
+
it("resolves with the underlying value if it settles before the timeout", function()
|
|
266
|
+
local source = Promise.new()
|
|
267
|
+
local promise = PromiseUtils.timeout(10, source)
|
|
268
|
+
expect(promise:IsPending()).toEqual(true)
|
|
269
|
+
|
|
270
|
+
source:Resolve("in time")
|
|
271
|
+
local outcome, value = PromiseTestUtils.awaitOutcome(promise)
|
|
272
|
+
expect(outcome).toEqual("resolved")
|
|
273
|
+
expect(value).toEqual("in time")
|
|
274
|
+
end)
|
|
275
|
+
end)
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
--!nonstrict
|
|
2
|
+
--[[
|
|
3
|
+
@class PendingPromiseTracker.spec.lua
|
|
4
|
+
]]
|
|
5
|
+
|
|
6
|
+
local require = require(script.Parent.loader).load(script)
|
|
7
|
+
|
|
8
|
+
local Jest = require("Jest")
|
|
9
|
+
local PendingPromiseTracker = require("PendingPromiseTracker")
|
|
10
|
+
local Promise = require("Promise")
|
|
11
|
+
|
|
12
|
+
local describe = Jest.Globals.describe
|
|
13
|
+
local expect = Jest.Globals.expect
|
|
14
|
+
local it = Jest.Globals.it
|
|
15
|
+
|
|
16
|
+
describe("PendingPromiseTracker", function()
|
|
17
|
+
it("starts empty", function()
|
|
18
|
+
local tracker = PendingPromiseTracker.new()
|
|
19
|
+
expect(#tracker:GetAll()).toEqual(0)
|
|
20
|
+
end)
|
|
21
|
+
|
|
22
|
+
it("tracks a pending promise", function()
|
|
23
|
+
local tracker = PendingPromiseTracker.new()
|
|
24
|
+
tracker:Add(Promise.new())
|
|
25
|
+
expect(#tracker:GetAll()).toEqual(1)
|
|
26
|
+
end)
|
|
27
|
+
|
|
28
|
+
it("ignores an already-settled promise", function()
|
|
29
|
+
local tracker = PendingPromiseTracker.new()
|
|
30
|
+
tracker:Add(Promise.resolved("done"))
|
|
31
|
+
expect(#tracker:GetAll()).toEqual(0)
|
|
32
|
+
end)
|
|
33
|
+
|
|
34
|
+
it("drops a promise once it resolves", function()
|
|
35
|
+
local tracker = PendingPromiseTracker.new()
|
|
36
|
+
local promise = Promise.new()
|
|
37
|
+
tracker:Add(promise)
|
|
38
|
+
expect(#tracker:GetAll()).toEqual(1)
|
|
39
|
+
|
|
40
|
+
promise:Resolve(true)
|
|
41
|
+
expect(#tracker:GetAll()).toEqual(0)
|
|
42
|
+
end)
|
|
43
|
+
|
|
44
|
+
it("drops a promise once it rejects", function()
|
|
45
|
+
local tracker = PendingPromiseTracker.new()
|
|
46
|
+
local promise = Promise.new()
|
|
47
|
+
promise:Catch(function() end)
|
|
48
|
+
tracker:Add(promise)
|
|
49
|
+
expect(#tracker:GetAll()).toEqual(1)
|
|
50
|
+
|
|
51
|
+
promise:Reject("boom")
|
|
52
|
+
expect(#tracker:GetAll()).toEqual(0)
|
|
53
|
+
end)
|
|
54
|
+
|
|
55
|
+
it("tracks multiple pending promises independently", function()
|
|
56
|
+
local tracker = PendingPromiseTracker.new()
|
|
57
|
+
local a = Promise.new()
|
|
58
|
+
local b = Promise.new()
|
|
59
|
+
tracker:Add(a)
|
|
60
|
+
tracker:Add(b)
|
|
61
|
+
expect(#tracker:GetAll()).toEqual(2)
|
|
62
|
+
|
|
63
|
+
a:Resolve(true)
|
|
64
|
+
expect(#tracker:GetAll()).toEqual(1)
|
|
65
|
+
|
|
66
|
+
b:Resolve(true)
|
|
67
|
+
expect(#tracker:GetAll()).toEqual(0)
|
|
68
|
+
end)
|
|
69
|
+
end)
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
--!nonstrict
|
|
2
|
+
--[[
|
|
3
|
+
@class PromiseInstanceUtils.spec.lua
|
|
4
|
+
]]
|
|
5
|
+
|
|
6
|
+
local require = require(script.Parent.loader).load(script)
|
|
7
|
+
|
|
8
|
+
local Jest = require("Jest")
|
|
9
|
+
local PromiseInstanceUtils = require("PromiseInstanceUtils")
|
|
10
|
+
local PromiseTestUtils = require("PromiseTestUtils")
|
|
11
|
+
|
|
12
|
+
local Workspace = game:GetService("Workspace")
|
|
13
|
+
|
|
14
|
+
local describe = Jest.Globals.describe
|
|
15
|
+
local expect = Jest.Globals.expect
|
|
16
|
+
local it = Jest.Globals.it
|
|
17
|
+
|
|
18
|
+
describe("PromiseInstanceUtils.promiseRemoved", function()
|
|
19
|
+
it("is pending while the instance stays parented", function()
|
|
20
|
+
local folder = Instance.new("Folder")
|
|
21
|
+
folder.Parent = Workspace
|
|
22
|
+
|
|
23
|
+
local promise = PromiseInstanceUtils.promiseRemoved(folder)
|
|
24
|
+
expect(promise:IsPending()).toEqual(true)
|
|
25
|
+
|
|
26
|
+
folder:Destroy()
|
|
27
|
+
end)
|
|
28
|
+
|
|
29
|
+
it("resolves when the instance is removed from the game", function()
|
|
30
|
+
local folder = Instance.new("Folder")
|
|
31
|
+
folder.Parent = Workspace
|
|
32
|
+
|
|
33
|
+
local promise = PromiseInstanceUtils.promiseRemoved(folder)
|
|
34
|
+
folder.Parent = nil
|
|
35
|
+
|
|
36
|
+
expect(PromiseTestUtils.awaitSettled(promise)).toEqual(true)
|
|
37
|
+
expect(promise:IsFulfilled()).toEqual(true)
|
|
38
|
+
|
|
39
|
+
folder:Destroy()
|
|
40
|
+
end)
|
|
41
|
+
end)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
--!nonstrict
|
|
2
|
+
--[[
|
|
3
|
+
@class promiseChild.spec.lua
|
|
4
|
+
]]
|
|
5
|
+
|
|
6
|
+
local require = require(script.Parent.loader).load(script)
|
|
7
|
+
|
|
8
|
+
local Jest = require("Jest")
|
|
9
|
+
local PromiseTestUtils = require("PromiseTestUtils")
|
|
10
|
+
local promiseChild = require("promiseChild")
|
|
11
|
+
|
|
12
|
+
local Workspace = game:GetService("Workspace")
|
|
13
|
+
|
|
14
|
+
local describe = Jest.Globals.describe
|
|
15
|
+
local expect = Jest.Globals.expect
|
|
16
|
+
local it = Jest.Globals.it
|
|
17
|
+
|
|
18
|
+
describe("promiseChild", function()
|
|
19
|
+
it("resolves immediately when the child already exists", function()
|
|
20
|
+
local parent = Instance.new("Folder")
|
|
21
|
+
local child = Instance.new("Folder")
|
|
22
|
+
child.Name = "Target"
|
|
23
|
+
child.Parent = parent
|
|
24
|
+
parent.Parent = Workspace
|
|
25
|
+
|
|
26
|
+
local promise = promiseChild(parent, "Target")
|
|
27
|
+
expect(promise:IsFulfilled()).toEqual(true)
|
|
28
|
+
|
|
29
|
+
local outcome, value = PromiseTestUtils.awaitOutcome(promise)
|
|
30
|
+
expect(outcome).toEqual("resolved")
|
|
31
|
+
expect(value).toEqual(child)
|
|
32
|
+
|
|
33
|
+
parent:Destroy()
|
|
34
|
+
end)
|
|
35
|
+
|
|
36
|
+
it("resolves once a matching child is added", function()
|
|
37
|
+
local parent = Instance.new("Folder")
|
|
38
|
+
parent.Parent = Workspace
|
|
39
|
+
|
|
40
|
+
local promise = promiseChild(parent, "Later")
|
|
41
|
+
expect(promise:IsPending()).toEqual(true)
|
|
42
|
+
|
|
43
|
+
local child = Instance.new("Folder")
|
|
44
|
+
child.Name = "Later"
|
|
45
|
+
child.Parent = parent
|
|
46
|
+
|
|
47
|
+
local outcome, value = PromiseTestUtils.awaitOutcome(promise)
|
|
48
|
+
expect(outcome).toEqual("resolved")
|
|
49
|
+
expect(value).toEqual(child)
|
|
50
|
+
|
|
51
|
+
parent:Destroy()
|
|
52
|
+
end)
|
|
53
|
+
|
|
54
|
+
it("rejects when the child never appears before the timeout", function()
|
|
55
|
+
local parent = Instance.new("Folder")
|
|
56
|
+
parent.Parent = Workspace
|
|
57
|
+
|
|
58
|
+
local outcome, err = PromiseTestUtils.awaitOutcome(promiseChild(parent, "Missing", 0.05))
|
|
59
|
+
expect(outcome).toEqual("rejected")
|
|
60
|
+
expect(string.find(tostring(err), "Timed out", 1, true) ~= nil).toEqual(true)
|
|
61
|
+
|
|
62
|
+
parent:Destroy()
|
|
63
|
+
end)
|
|
64
|
+
end)
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
--!nonstrict
|
|
2
|
+
--[[
|
|
3
|
+
@class promisePropertyValue.spec.lua
|
|
4
|
+
]]
|
|
5
|
+
|
|
6
|
+
local require = require(script.Parent.loader).load(script)
|
|
7
|
+
|
|
8
|
+
local Jest = require("Jest")
|
|
9
|
+
local PromiseTestUtils = require("PromiseTestUtils")
|
|
10
|
+
local promisePropertyValue = require("promisePropertyValue")
|
|
11
|
+
|
|
12
|
+
local Workspace = game:GetService("Workspace")
|
|
13
|
+
|
|
14
|
+
local describe = Jest.Globals.describe
|
|
15
|
+
local expect = Jest.Globals.expect
|
|
16
|
+
local it = Jest.Globals.it
|
|
17
|
+
|
|
18
|
+
describe("promisePropertyValue", function()
|
|
19
|
+
it("resolves immediately when the property is already truthy", function()
|
|
20
|
+
local folder = Instance.new("Folder")
|
|
21
|
+
folder.Name = "HasAName"
|
|
22
|
+
folder.Parent = Workspace
|
|
23
|
+
|
|
24
|
+
local promise = promisePropertyValue(folder, "Name")
|
|
25
|
+
expect(promise:IsFulfilled()).toEqual(true)
|
|
26
|
+
|
|
27
|
+
local outcome, value = PromiseTestUtils.awaitOutcome(promise)
|
|
28
|
+
expect(outcome).toEqual("resolved")
|
|
29
|
+
expect(value).toEqual("HasAName")
|
|
30
|
+
|
|
31
|
+
folder:Destroy()
|
|
32
|
+
end)
|
|
33
|
+
|
|
34
|
+
it("resolves once a falsy property becomes truthy", function()
|
|
35
|
+
local objectValue = Instance.new("ObjectValue")
|
|
36
|
+
objectValue.Parent = Workspace
|
|
37
|
+
expect(objectValue.Value).toEqual(nil)
|
|
38
|
+
|
|
39
|
+
local promise = promisePropertyValue(objectValue, "Value")
|
|
40
|
+
expect(promise:IsPending()).toEqual(true)
|
|
41
|
+
|
|
42
|
+
local target = Instance.new("Folder")
|
|
43
|
+
target.Parent = Workspace
|
|
44
|
+
objectValue.Value = target
|
|
45
|
+
|
|
46
|
+
local outcome, value = PromiseTestUtils.awaitOutcome(promise)
|
|
47
|
+
expect(outcome).toEqual("resolved")
|
|
48
|
+
expect(value).toEqual(target)
|
|
49
|
+
|
|
50
|
+
objectValue:Destroy()
|
|
51
|
+
target:Destroy()
|
|
52
|
+
end)
|
|
53
|
+
end)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
--!nonstrict
|
|
2
|
+
--[[
|
|
3
|
+
@class promiseWait.spec.lua
|
|
4
|
+
]]
|
|
5
|
+
|
|
6
|
+
local require = require(script.Parent.loader).load(script)
|
|
7
|
+
|
|
8
|
+
local Jest = require("Jest")
|
|
9
|
+
local PromiseTestUtils = require("PromiseTestUtils")
|
|
10
|
+
local promiseWait = require("promiseWait")
|
|
11
|
+
|
|
12
|
+
local describe = Jest.Globals.describe
|
|
13
|
+
local expect = Jest.Globals.expect
|
|
14
|
+
local it = Jest.Globals.it
|
|
15
|
+
|
|
16
|
+
describe("promiseWait", function()
|
|
17
|
+
it("resolves after the delay elapses", function()
|
|
18
|
+
local outcome = PromiseTestUtils.awaitOutcome(promiseWait(0))
|
|
19
|
+
expect(outcome).toEqual("resolved")
|
|
20
|
+
end)
|
|
21
|
+
|
|
22
|
+
it("is pending before the delay elapses", function()
|
|
23
|
+
local promise = promiseWait(10)
|
|
24
|
+
expect(promise:IsPending()).toEqual(true)
|
|
25
|
+
end)
|
|
26
|
+
end)
|