ssr-bundle-render.spec.ts 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325
  1. // @vitest-environment node
  2. import { createWebpackBundleRenderer } from './compile-with-webpack'
  3. describe('SSR: bundle renderer', () => {
  4. createAssertions(true)
  5. // createAssertions(false)
  6. })
  7. function createAssertions(runInNewContext) {
  8. it('renderToString', async () => {
  9. const renderer = await createWebpackBundleRenderer('app.js', {
  10. runInNewContext
  11. })
  12. const context: any = { url: '/test' }
  13. const res = await renderer.renderToString(context)
  14. expect(res).toBe('<div data-server-rendered="true">/test</div>')
  15. expect(context.msg).toBe('hello')
  16. })
  17. it('renderToStream', async () => {
  18. const renderer = await createWebpackBundleRenderer('app.js', {
  19. runInNewContext
  20. })
  21. const context: any = { url: '/test' }
  22. const res = await new Promise((resolve, reject) => {
  23. const stream = renderer.renderToStream(context)
  24. let res = ''
  25. stream.on('data', chunk => {
  26. res += chunk.toString()
  27. })
  28. stream.on('error', reject)
  29. stream.on('end', () => {
  30. resolve(res)
  31. })
  32. })
  33. expect(res).toBe('<div data-server-rendered="true">/test</div>')
  34. expect(context.msg).toBe('hello')
  35. })
  36. it('renderToString catch error', async () => {
  37. const renderer = await createWebpackBundleRenderer('error.js', {
  38. runInNewContext
  39. })
  40. try {
  41. await renderer.renderToString()
  42. } catch (err: any) {
  43. expect(err.message).toBe('foo')
  44. }
  45. })
  46. it('renderToString catch Promise rejection', async () => {
  47. const renderer = await createWebpackBundleRenderer('promise-rejection.js', {
  48. runInNewContext
  49. })
  50. try {
  51. await renderer.renderToString()
  52. } catch (err: any) {
  53. expect(err.message).toBe('foo')
  54. }
  55. })
  56. it('renderToStream catch error', async () => {
  57. const renderer = await createWebpackBundleRenderer('error.js', {
  58. runInNewContext
  59. })
  60. const err = await new Promise<Error>(resolve => {
  61. const stream = renderer.renderToStream()
  62. stream.on('error', resolve)
  63. })
  64. expect(err.message).toBe('foo')
  65. })
  66. it('renderToStream catch Promise rejection', async () => {
  67. const renderer = await createWebpackBundleRenderer('promise-rejection.js', {
  68. runInNewContext
  69. })
  70. const err = await new Promise<Error>(resolve => {
  71. const stream = renderer.renderToStream()
  72. stream.on('error', resolve)
  73. })
  74. expect(err.message).toBe('foo')
  75. })
  76. it('render with cache (get/set)', async () => {
  77. const cache = {}
  78. const get = vi.fn()
  79. const set = vi.fn()
  80. const options = {
  81. runInNewContext,
  82. cache: {
  83. // async
  84. get: (key, cb) => {
  85. setTimeout(() => {
  86. get(key)
  87. cb(cache[key])
  88. }, 0)
  89. },
  90. set: (key, val) => {
  91. set(key, val)
  92. cache[key] = val
  93. }
  94. }
  95. }
  96. const renderer = await createWebpackBundleRenderer('cache.js', options)
  97. const expected = '<div data-server-rendered="true">/test</div>'
  98. const key = 'app::1'
  99. const res = await renderer.renderToString()
  100. expect(res).toBe(expected)
  101. expect(get).toHaveBeenCalledWith(key)
  102. const setArgs = set.mock.calls[0]
  103. expect(setArgs[0]).toBe(key)
  104. expect(setArgs[1].html).toBe(expected)
  105. expect(cache[key].html).toBe(expected)
  106. const res2 = await renderer.renderToString()
  107. expect(res2).toBe(expected)
  108. expect(get.mock.calls.length).toBe(2)
  109. expect(set.mock.calls.length).toBe(1)
  110. })
  111. it('render with cache (get/set/has)', async () => {
  112. const cache = {}
  113. const has = vi.fn()
  114. const get = vi.fn()
  115. const set = vi.fn()
  116. const options = {
  117. runInNewContext,
  118. cache: {
  119. // async
  120. has: (key, cb) => {
  121. has(key)
  122. cb(!!cache[key])
  123. },
  124. // sync
  125. get: key => {
  126. get(key)
  127. return cache[key]
  128. },
  129. set: (key, val) => {
  130. set(key, val)
  131. cache[key] = val
  132. }
  133. }
  134. }
  135. const renderer = await createWebpackBundleRenderer('cache.js', options)
  136. const expected = '<div data-server-rendered="true">/test</div>'
  137. const key = 'app::1'
  138. const res = await renderer.renderToString()
  139. expect(res).toBe(expected)
  140. expect(has).toHaveBeenCalledWith(key)
  141. expect(get).not.toHaveBeenCalled()
  142. const setArgs = set.mock.calls[0]
  143. expect(setArgs[0]).toBe(key)
  144. expect(setArgs[1].html).toBe(expected)
  145. expect(cache[key].html).toBe(expected)
  146. const res2 = await renderer.renderToString()
  147. expect(res2).toBe(expected)
  148. expect(has.mock.calls.length).toBe(2)
  149. expect(get.mock.calls.length).toBe(1)
  150. expect(set.mock.calls.length).toBe(1)
  151. })
  152. it('render with cache (nested)', async () => {
  153. const cache = new Map() as any
  154. vi.spyOn(cache, 'get')
  155. vi.spyOn(cache, 'set')
  156. const options = {
  157. cache,
  158. runInNewContext
  159. }
  160. const renderer = await createWebpackBundleRenderer(
  161. 'nested-cache.js',
  162. options
  163. )
  164. const expected = '<div data-server-rendered="true">/test</div>'
  165. const key = 'app::1'
  166. const context1 = { registered: [] }
  167. const context2 = { registered: [] }
  168. const res = await renderer.renderToString(context1)
  169. expect(res).toBe(expected)
  170. expect(cache.set.mock.calls.length).toBe(3) // 3 nested components cached
  171. const cached = cache.get(key)
  172. expect(cached.html).toBe(expected)
  173. expect(cache.get.mock.calls.length).toBe(1)
  174. // assert component usage registration for nested children
  175. expect(context1.registered).toEqual(['app', 'child', 'grandchild'])
  176. const res2 = await renderer.renderToString(context2)
  177. expect(res2).toBe(expected)
  178. expect(cache.set.mock.calls.length).toBe(3) // no new cache sets
  179. expect(cache.get.mock.calls.length).toBe(2) // 1 get for root
  180. expect(context2.registered).toEqual(['app', 'child', 'grandchild'])
  181. })
  182. it('render with cache (opt-out)', async () => {
  183. const cache = {}
  184. const get = vi.fn()
  185. const set = vi.fn()
  186. const options = {
  187. runInNewContext,
  188. cache: {
  189. // async
  190. get: (key, cb) => {
  191. setTimeout(() => {
  192. get(key)
  193. cb(cache[key])
  194. }, 0)
  195. },
  196. set: (key, val) => {
  197. set(key, val)
  198. cache[key] = val
  199. }
  200. }
  201. }
  202. const renderer = await createWebpackBundleRenderer(
  203. 'cache-opt-out.js',
  204. options
  205. )
  206. const expected = '<div data-server-rendered="true">/test</div>'
  207. const res = await renderer.renderToString()
  208. expect(res).toBe(expected)
  209. expect(get).not.toHaveBeenCalled()
  210. expect(set).not.toHaveBeenCalled()
  211. const res2 = await renderer.renderToString()
  212. expect(res2).toBe(expected)
  213. expect(get).not.toHaveBeenCalled()
  214. expect(set).not.toHaveBeenCalled()
  215. })
  216. it('renderToString (bundle format with code split)', async () => {
  217. const renderer = await createWebpackBundleRenderer('split.js', {
  218. runInNewContext,
  219. asBundle: true
  220. })
  221. const context = { url: '/test' }
  222. const res = await renderer.renderToString(context)
  223. expect(res).toBe(
  224. '<div data-server-rendered="true">/test<div>async test.woff2 test.png</div></div>'
  225. )
  226. })
  227. it('renderToStream (bundle format with code split)', async () => {
  228. const renderer = await createWebpackBundleRenderer('split.js', {
  229. runInNewContext,
  230. asBundle: true
  231. })
  232. const context = { url: '/test' }
  233. const res = await new Promise((resolve, reject) => {
  234. const stream = renderer.renderToStream(context)
  235. let res = ''
  236. stream.on('data', chunk => {
  237. res += chunk.toString()
  238. })
  239. stream.on('error', reject)
  240. stream.on('end', () => {
  241. resolve(res)
  242. })
  243. })
  244. expect(res).toBe(
  245. '<div data-server-rendered="true">/test<div>async test.woff2 test.png</div></div>'
  246. )
  247. })
  248. it('renderToString catch error (bundle format with source map)', async () => {
  249. const renderer = await createWebpackBundleRenderer('error.js', {
  250. runInNewContext,
  251. asBundle: true
  252. })
  253. try {
  254. await renderer.renderToString()
  255. } catch (err: any) {
  256. expect(err.stack).toContain('server-renderer/test/fixtures/error.js:1:0')
  257. expect(err.message).toBe('foo')
  258. }
  259. })
  260. it('renderToStream catch error (bundle format with source map)', async () => {
  261. const renderer = await createWebpackBundleRenderer('error.js', {
  262. runInNewContext,
  263. asBundle: true
  264. })
  265. const err = await new Promise<Error>(resolve => {
  266. const stream = renderer.renderToStream()
  267. stream.on('error', resolve)
  268. })
  269. expect(err.stack).toContain('server-renderer/test/fixtures/error.js:1:0')
  270. expect(err.message).toBe('foo')
  271. })
  272. it('renderToString w/ callback', async () => {
  273. const renderer = await createWebpackBundleRenderer('app.js', {
  274. runInNewContext
  275. })
  276. const context: any = { url: '/test' }
  277. const res = await new Promise(r =>
  278. renderer.renderToString(context, (_err, res) => r(res))
  279. )
  280. expect(res).toBe('<div data-server-rendered="true">/test</div>')
  281. expect(context.msg).toBe('hello')
  282. })
  283. it('renderToString error handling w/ callback', async () => {
  284. const renderer = await createWebpackBundleRenderer('error.js', {
  285. runInNewContext
  286. })
  287. const err = await new Promise<Error>(r => renderer.renderToString(r))
  288. expect(err.message).toBe('foo')
  289. })
  290. }