ViewTest.swift 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317
  1. //
  2. // ViewTest.swift
  3. // ark_ui_basic
  4. //
  5. // Status: Complete
  6. // ID: 1FCA4829BCDAAC91F1E6D1FB696F6642 (SwiftUI)
  7. public import Foundation
  8. @_spi(ForOpenSwiftUIOnly)
  9. import ark_ui_basic_core
  10. #if os(iOS) || os(visionOS)
  11. public import UIKit
  12. #endif
  13. // MARK: - _ViewTest [6.4.41]
  14. @available(OpenSwiftUI_v1_0, *)
  15. public protocol _ViewTest: _Test {
  16. associatedtype RootView: View
  17. associatedtype RootStateType = Void
  18. func initRootView() -> Self.RootView
  19. func initSize() -> CGSize
  20. func setTestView<V>(_ view: V) where V: View
  21. }
  22. #if os(iOS) || os(visionOS)
  23. private enum Error: Swift.Error {
  24. case failedToReenableAnimations(String)
  25. case failedToDismissPresentation(String)
  26. }
  27. #endif
  28. @available(OpenSwiftUI_v1_0, *)
  29. extension _ViewTest {
  30. public func setUpTest() {
  31. setEnvironment(EnvironmentValues())
  32. setSize(initSize())
  33. setSafeAreaInsets(.zero)
  34. setRootTestView(initRootView())
  35. withRenderOptions(.simple) {
  36. render()
  37. }
  38. }
  39. public func tearDownTest() {
  40. resetEvents()
  41. setRootTestView(EmptyView())
  42. #if os(iOS) || os(visionOS)
  43. func performRender() {
  44. withRenderOptions(.simple) {
  45. render()
  46. }
  47. }
  48. UIView.performWithoutAnimation {
  49. performRender()
  50. }
  51. #endif
  52. }
  53. @available(OpenSwiftUI_v4_4, *)
  54. public func tearDownTestWithError() throws {
  55. #if os(iOS) || os(visionOS)
  56. guard !UIView.areAnimationsEnabled else {
  57. return
  58. }
  59. UIView.setAnimationsEnabled(true)
  60. throw Error.failedToReenableAnimations(String(describing: self))
  61. #endif
  62. }
  63. public func setTestView<V>(_ view: V) where V: View {
  64. setRootTestView(view)
  65. }
  66. public var rootView: Self.RootView {
  67. withRenderIfNeeded {
  68. _TestApp.host!.viewForIdentifier(
  69. rootViewID,
  70. RootView.self
  71. )
  72. }!
  73. }
  74. private var rootViewID: Int {
  75. findState()!.wrappedValue.id
  76. }
  77. private func setRootTestView<V>(_ view: V) where V: View {
  78. let state = findState()!
  79. let host = _TestApp.host!
  80. if let viewRendererHost = host as? ViewRendererHost {
  81. viewRendererHost.currentTimestamp = Time(seconds: ceil(viewRendererHost.currentTimestamp.seconds + 1.0))
  82. }
  83. state.wrappedValue.setTestView(view)
  84. }
  85. private func findState() -> Binding<_TestApp.RootView.StateType>? {
  86. withRenderIfNeeded {
  87. _TestApp.host!.stateForIdentifier(
  88. _TestApp.rootViewIdentifier,
  89. type: _TestApp.RootView.StateType.self,
  90. in: _TestApp.RootView.self
  91. )
  92. }
  93. }
  94. public func viewForIdentifier<V, I>(
  95. _ identifier: I,
  96. _ type: V.Type = V.self
  97. ) -> V? where V: View, I: Hashable {
  98. withRenderIfNeeded {
  99. _TestApp.host!.viewForIdentifier(identifier, type)
  100. }
  101. }
  102. public func stateForIdentifier<I, S, V>(
  103. _ id: I,
  104. type stateType: S.Type,
  105. in viewType: V.Type
  106. ) -> Binding<S>? where I: Hashable, V: View {
  107. withRenderIfNeeded {
  108. _TestApp.host!.stateForIdentifier(id, type: stateType, in: viewType)
  109. }
  110. }
  111. private func withRenderIfNeeded<V>(_ body: () -> V?) -> V? {
  112. if let value = body() {
  113. return value
  114. } else {
  115. _TestApp.host!.renderForTest(interval: .zero)
  116. return body()
  117. }
  118. }
  119. public func render(seconds: Double = 1.0 / 60.0) {
  120. let renderOptions = _TestApp.renderOptions
  121. let host = renderOptions.contains(.comparison) ? _TestApp.comparisonHost! : _TestApp.host!
  122. render(
  123. host: host,
  124. seconds: seconds,
  125. options: renderOptions
  126. )
  127. let isPostRenderRunLoop = renderOptions.contains(.postRenderRunLoop)
  128. if isPostRenderRunLoop {
  129. turnRunLoopIfNeeded(host: host, seconds: seconds, options: renderOptions)
  130. }
  131. }
  132. @available(OpenSwiftUI_v3_0, *)
  133. public func renderAsync(seconds: Double = 1.0 / 60.0) -> Bool {
  134. render(host: _TestApp.host!, seconds: seconds, options: [.async])
  135. }
  136. @available(OpenSwiftUI_v5_0, *)
  137. public func renderRecursively(seconds: Double = 1.0 / 60.0) {
  138. render(host: _TestApp.host!, seconds: seconds, options: [.recursive])
  139. }
  140. @discardableResult
  141. private func render(host: any TestHost, seconds: Double, options: TestRenderOptions) -> Bool {
  142. let isRecursive = options.contains(.recursive)
  143. if isRecursive {
  144. var result = true
  145. host.forEachDescendantHost { host in
  146. if options.contains(.async) {
  147. if !host._renderAsyncForTest(interval: seconds) {
  148. result = false
  149. }
  150. } else {
  151. host.renderForTest(interval: seconds)
  152. }
  153. }
  154. return result
  155. } else {
  156. if options.contains(.async) {
  157. return host._renderAsyncForTest(interval: seconds)
  158. } else {
  159. host.renderForTest(interval: seconds)
  160. return true
  161. }
  162. }
  163. }
  164. public func initSize() -> CGSize {
  165. CGSize(width: 100, height: 100)
  166. }
  167. public func setSize(_ size: CGSize) {
  168. _TestApp.host!.setTestSize(size)
  169. _TestApp.comparisonHost?.setTestSize(size)
  170. }
  171. func setSafeAreaInsets(_ insets: EdgeInsets) {
  172. _TestApp.host!.setTestSafeAreaInsets(insets)
  173. }
  174. public func setEnvironment(_ environment: EnvironmentValues?) {
  175. _TestApp.setTestEnvironment(environment)
  176. }
  177. #if os(iOS) || os(visionOS)
  178. public var systemColorScheme: UIUserInterfaceStyle? {
  179. let view = _TestApp.host! as! UIView
  180. guard let window = view.window,
  181. let windowScene = window.windowScene else {
  182. return nil
  183. }
  184. return windowScene._systemUserInterfaceStyle
  185. }
  186. #endif
  187. public func updateEnvironment(_ body: (inout EnvironmentValues) -> Void) {
  188. _TestApp.updateTestEnvironment(body)
  189. }
  190. public func resetEvents() {
  191. _TestApp.host!.resetTestEvents()
  192. }
  193. public func loop() {
  194. render()
  195. let defaultMode = RunLoop.Mode.default
  196. let commonMode = RunLoop.Mode.common
  197. var count: UInt = 0
  198. let interval = 0.001
  199. while true {
  200. let date = Date(timeIntervalSinceNow: interval)
  201. if !RunLoop.current.run(mode: count & 1 == 0 ? defaultMode : commonMode, before: date) {
  202. Thread.sleep(forTimeInterval: interval)
  203. }
  204. count += 1
  205. }
  206. }
  207. public func turnRunloop(times: Int = 1) {
  208. Swift.assert(times > 0)
  209. let defaultMode = RunLoop.Mode.default
  210. let commonMode = RunLoop.Mode.common
  211. let interval = 0.001
  212. var times = times
  213. while times != 0 {
  214. times -= 1
  215. // let modes = [defaultMode, commonMode]
  216. let date = Date(timeIntervalSinceNow: interval)
  217. if !RunLoop.current.run(mode: defaultMode, before: date) {
  218. Thread.sleep(forTimeInterval: interval)
  219. }
  220. }
  221. }
  222. private func turnRunLoopIfNeeded(host: any TestHost, seconds: Double, options: TestRenderOptions) {
  223. guard CoreTesting.neeedsRunLoopTurn else {
  224. return
  225. }
  226. let defaultMode = RunLoop.Mode.default
  227. let commonMode = RunLoop.Mode.common
  228. let interval = 0.001
  229. var times = 17
  230. while CoreTesting.needsRender || CoreTesting.neeedsRunLoopTurn {
  231. // let modes = [defaultMode, commonMode]
  232. let date = Date(timeIntervalSinceNow: interval)
  233. if !RunLoop.current.run(mode: defaultMode, before: date) {
  234. Thread.sleep(forTimeInterval: interval)
  235. }
  236. render(host: host, seconds: seconds, options: options)
  237. times &-= 1
  238. if times <= 1 {
  239. break
  240. }
  241. }
  242. if CoreTesting.needsRender || CoreTesting.neeedsRunLoopTurn {
  243. Log.unitTests.log("Render or run loop turn needed after max iterations")
  244. }
  245. }
  246. }
  247. extension _ViewTest {
  248. public func rootState<S>(type: S.Type = S.self) -> Binding<S> {
  249. withRenderIfNeeded {
  250. _TestApp.host!.stateForIdentifier(
  251. rootViewID,
  252. type: type,
  253. in: RootView.self
  254. )
  255. }!
  256. }
  257. public func rootState<S, V>(
  258. type stateType: S.Type = S.self,
  259. in viewType: V.Type
  260. ) -> Binding<S> where V: View {
  261. withRenderIfNeeded {
  262. _TestApp.host!.stateForIdentifier(
  263. rootViewID,
  264. type: stateType,
  265. in: viewType
  266. )
  267. }!
  268. }
  269. }
  270. extension _ViewTest {
  271. public func set<V>(
  272. _ keyPath: WritableKeyPath<RootStateType, V>,
  273. to value: V
  274. ) {
  275. rootState(type: RootStateType.self).wrappedValue[keyPath: keyPath] = value
  276. }
  277. public func get<V>(_ keyPath: KeyPath<RootStateType, V>) -> V {
  278. rootState(type: RootStateType.self).wrappedValue[keyPath: keyPath]
  279. }
  280. }