Skip to content

Commit 62a8dfb

Browse files
committed
sjsJUnitTest: copy-paste PromiseMock from scala.js upstream
To improve our testing of unions until we get a release of Scala.js with the fixed PromiseMock. This also lets us re-enable AsyncTest which depends on PromiseMock.
1 parent fbb687b commit 62a8dfb

File tree

2 files changed

+262
-1
lines changed

2 files changed

+262
-1
lines changed

project/Build.scala

+2-1
Original file line numberDiff line numberDiff line change
@@ -1105,7 +1105,8 @@ object Build {
11051105
-- "ObjectTest.scala" // compile errors caused by #9588
11061106
-- "StackTraceTest.scala" // would require `npm install source-map-support`
11071107
-- "UnionTypeTest.scala" // requires the Scala 2 macro defined in Typechecking*.scala
1108-
-- "PromiseMock.scala" -- "AsyncTest.scala" // TODO: Enable once we use a Scala.js with https://github.com/scala-js/scala-js/pull/4451 in
1108+
-- "PromiseMock.scala" // TODO: Enable once we use a Scala.js with https://github.com/scala-js/scala-js/pull/4451 in
1109+
// and remove copy in tests/sjs-junit
11091110
)).get
11101111

11111112
++ (dir / "js/src/test/require-2.12" ** (("*.scala": FileFilter)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
/*
2+
* Scala.js (https://www.scala-js.org/)
3+
*
4+
* Copyright EPFL.
5+
*
6+
* Licensed under Apache License 2.0
7+
* (https://www.apache.org/licenses/LICENSE-2.0).
8+
*
9+
* See the NOTICE file distributed with this work for
10+
* additional information regarding copyright ownership.
11+
*/
12+
13+
package org.scalajs.testsuite.jsinterop
14+
15+
import scala.scalajs.js
16+
import scala.scalajs.js.annotation._
17+
import scala.scalajs.js.|
18+
19+
import js.Thenable
20+
21+
object PromiseMock {
22+
23+
@noinline
24+
def withMockedPromise[A](body: (() => Unit) => A): A = {
25+
val global = org.scalajs.testsuite.utils.JSUtils.globalObject
26+
27+
val oldPromise =
28+
if (global.hasOwnProperty("Promise").asInstanceOf[Boolean]) Some(global.Promise)
29+
else None
30+
31+
global.Promise = js.constructorOf[MockPromise[_]]
32+
try {
33+
body(MockPromise.processQueue _)
34+
} finally {
35+
oldPromise.fold {
36+
js.special.delete(global, "Promise")
37+
} { old =>
38+
global.Promise = old
39+
}
40+
}
41+
}
42+
43+
@noinline
44+
def withMockedPromiseIfExists[A](body: (Option[() => Unit]) => A): A = {
45+
val global = org.scalajs.testsuite.utils.JSUtils.globalObject
46+
47+
val oldPromise = global.Promise
48+
49+
if (js.isUndefined(oldPromise)) {
50+
body(None)
51+
} else {
52+
global.Promise = js.constructorOf[MockPromise[_]]
53+
try {
54+
body(Some(MockPromise.processQueue _))
55+
} finally {
56+
global.Promise = oldPromise
57+
}
58+
}
59+
}
60+
61+
private object MockPromise {
62+
private val queue = js.Array[js.Function0[Any]]()
63+
64+
@JSExportStatic
65+
def resolve[A](value: A | js.Thenable[A]): MockPromise[A] = {
66+
new MockPromise[A]({
67+
(resolve: js.Function1[A | js.Thenable[A], _],
68+
reject: js.Function1[Any, _]) =>
69+
resolve(value)
70+
})
71+
}
72+
73+
@JSExportStatic
74+
def reject(reason: Any): MockPromise[Nothing] = {
75+
new MockPromise[Nothing]({
76+
(resolve: js.Function1[Nothing | js.Thenable[Nothing], _],
77+
reject: js.Function1[Any, _]) =>
78+
reject(reason)
79+
})
80+
}
81+
82+
def enqueue(f: js.Function0[Any]): Unit =
83+
queue.push(f)
84+
85+
def processQueue(): Unit = {
86+
while (queue.nonEmpty)
87+
queue.shift()()
88+
}
89+
90+
private sealed abstract class State[+A]
91+
92+
private case object Pending extends State[Nothing]
93+
private case class Fulfilled[+A](value: A) extends State[A]
94+
private case class Rejected(reason: Any) extends State[Nothing]
95+
96+
private def isNotAnObject(x: Any): Boolean = x match {
97+
case null | () | _:Double | _:Boolean | _:String => true
98+
case _ => false
99+
}
100+
101+
private def isCallable(x: Any): Boolean =
102+
js.typeOf(x.asInstanceOf[js.Any]) == "function"
103+
104+
private def throwAny(e: Any): Nothing = {
105+
throw (e match {
106+
case th: Throwable => th
107+
case _ => js.JavaScriptException(e)
108+
})
109+
}
110+
111+
private def tryCatchAny[A](tryBody: => A)(catchBody: Any => A): A = {
112+
try {
113+
tryBody
114+
} catch {
115+
case th: Throwable =>
116+
catchBody(th match {
117+
case js.JavaScriptException(e) => e
118+
case _ => th
119+
})
120+
}
121+
}
122+
}
123+
124+
private class MockPromise[+A](
125+
executor: js.Function2[js.Function1[A | Thenable[A], _], js.Function1[scala.Any, _], _])
126+
extends js.Object with js.Thenable[A] {
127+
128+
import MockPromise._
129+
130+
private[this] var state: State[A] = Pending
131+
132+
private[this] var fulfillReactions = js.Array[js.Function1[A, Any]]()
133+
private[this] var rejectReactions = js.Array[js.Function1[Any, Any]]()
134+
135+
init(executor)
136+
137+
// 25.4.3.1 Promise(executor)
138+
private[this] def init(
139+
executor: js.Function2[js.Function1[A | Thenable[A], _], js.Function1[scala.Any, _], _]) = {
140+
tryCatchAny[Unit] {
141+
executor(resolve _, reject _)
142+
} { e =>
143+
reject(e)
144+
}
145+
}
146+
147+
private[this] def fulfill(value: A): Unit = {
148+
assert(state == Pending)
149+
state = Fulfilled(value)
150+
clearAndTriggerReactions(fulfillReactions, value)
151+
}
152+
153+
private[this] def clearAndTriggerReactions[A](
154+
reactions: js.Array[js.Function1[A, Any]],
155+
argument: A): Unit = {
156+
157+
assert(state != Pending)
158+
159+
fulfillReactions = null
160+
rejectReactions = null
161+
162+
for (reaction <- reactions)
163+
enqueue(() => reaction(argument))
164+
}
165+
166+
// 25.4.1.3.2 Promise Resolve Functions
167+
private[this] def resolve(resolution: A | Thenable[A]): Unit = {
168+
if (state == Pending) {
169+
if (resolution.asInstanceOf[AnyRef] eq this) {
170+
reject(new js.TypeError("Self resolution"))
171+
} else if (isNotAnObject(resolution)) {
172+
fulfill(resolution.asInstanceOf[A])
173+
} else {
174+
tryCatchAny {
175+
val thenAction = resolution.asInstanceOf[js.Dynamic].`then`
176+
if (!isCallable(thenAction)) {
177+
fulfill(resolution.asInstanceOf[A])
178+
} else {
179+
val thenable = resolution.asInstanceOf[Thenable[A]]
180+
val thenActionFun = thenAction.asInstanceOf[js.Function]
181+
enqueue(() => promiseResolveThenableJob(thenable, thenActionFun))
182+
}
183+
} { e =>
184+
reject(e)
185+
}
186+
}
187+
}
188+
}
189+
190+
// 25.4.2.2 PromiseResolveThenableJob
191+
private[this] def promiseResolveThenableJob(thenable: Thenable[A],
192+
thenAction: js.Function): Unit = {
193+
thenAction.call(thenable, resolve _, reject _)
194+
}
195+
196+
// 25.4.1.3.1 Promise Reject Functions
197+
private[this] def reject(reason: Any): Unit = {
198+
if (state == Pending) {
199+
state = Rejected(reason)
200+
clearAndTriggerReactions(rejectReactions, reason)
201+
}
202+
}
203+
204+
// 25.4.5.3 Promise.prototype.then
205+
def `then`[B](
206+
onFulfilled: js.Function1[A, B | Thenable[B]],
207+
onRejected: js.UndefOr[js.Function1[scala.Any, B | Thenable[B]]]): MockPromise[B] = {
208+
209+
new MockPromise[B](
210+
{ (innerResolve: js.Function1[B | Thenable[B], _],
211+
innerReject: js.Function1[scala.Any, _]) =>
212+
213+
def doFulfilled(value: A): Unit = {
214+
tryCatchAny[Unit] {
215+
innerResolve(onFulfilled(value))
216+
} { e =>
217+
innerReject(e)
218+
}
219+
}
220+
221+
def doRejected(reason: Any): Unit = {
222+
tryCatchAny[Unit] {
223+
onRejected.fold[Unit] {
224+
innerReject(reason)
225+
} { onRejectedFun =>
226+
innerResolve(onRejectedFun(reason))
227+
}
228+
} { e =>
229+
innerReject(e)
230+
}
231+
}
232+
233+
state match {
234+
case Pending =>
235+
fulfillReactions += doFulfilled _
236+
rejectReactions += doRejected _
237+
238+
case Fulfilled(value) =>
239+
enqueue(() => doFulfilled(value))
240+
241+
case Rejected(reason) =>
242+
enqueue(() => doRejected(reason))
243+
}
244+
}
245+
)
246+
}
247+
248+
def `then`[B >: A](
249+
onFulfilled: Unit,
250+
onRejected: js.UndefOr[js.Function1[scala.Any, B | Thenable[B]]]): MockPromise[B] = {
251+
`then`((x: A) => (x: B | Thenable[B]), onRejected)
252+
}
253+
254+
// 25.4.5.1 Promise.prototype.catch
255+
def `catch`[B >: A](
256+
onRejected: js.UndefOr[js.Function1[scala.Any, B | Thenable[B]]]): MockPromise[B] = {
257+
`then`((), onRejected)
258+
}
259+
}
260+
}

0 commit comments

Comments
 (0)