ssrDirectives.spec.ts 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588
  1. import { renderToString } from '../src/renderToString'
  2. import {
  3. createApp,
  4. h,
  5. mergeProps,
  6. ref,
  7. resolveDirective,
  8. unref,
  9. vModelCheckbox,
  10. vModelDynamic,
  11. vModelRadio,
  12. vModelText,
  13. vShow,
  14. withDirectives,
  15. } from 'vue'
  16. import { ssrGetDirectiveProps, ssrRenderAttrs } from '../src'
  17. describe('ssr: directives', () => {
  18. describe('template v-show', () => {
  19. test('basic', async () => {
  20. expect(
  21. await renderToString(
  22. createApp({
  23. template: `<div v-show="true"/>`,
  24. }),
  25. ),
  26. ).toBe(`<div style=""></div>`)
  27. expect(
  28. await renderToString(
  29. createApp({
  30. template: `<div v-show="false"/>`,
  31. }),
  32. ),
  33. ).toBe(`<div style="display:none;"></div>`)
  34. })
  35. test('with static style', async () => {
  36. expect(
  37. await renderToString(
  38. createApp({
  39. template: `<div style="color:red" v-show="false"/>`,
  40. }),
  41. ),
  42. ).toBe(`<div style="color:red;display:none;"></div>`)
  43. })
  44. test('with dynamic style', async () => {
  45. expect(
  46. await renderToString(
  47. createApp({
  48. data: () => ({ style: { color: 'red' } }),
  49. template: `<div :style="style" v-show="false"/>`,
  50. }),
  51. ),
  52. ).toBe(`<div style="color:red;display:none;"></div>`)
  53. })
  54. test('with static + dynamic style', async () => {
  55. expect(
  56. await renderToString(
  57. createApp({
  58. data: () => ({ style: { color: 'red' } }),
  59. template: `<div :style="style" style="font-size:12;" v-show="false"/>`,
  60. }),
  61. ),
  62. ).toBe(`<div style="color:red;font-size:12;display:none;"></div>`)
  63. })
  64. })
  65. describe('template v-model', () => {
  66. test('text', async () => {
  67. expect(
  68. await renderToString(
  69. createApp({
  70. data: () => ({ text: 'hello' }),
  71. template: `<input v-model="text">`,
  72. }),
  73. ),
  74. ).toBe(`<input value="hello">`)
  75. })
  76. test('radio', async () => {
  77. expect(
  78. await renderToString(
  79. createApp({
  80. data: () => ({ selected: 'foo' }),
  81. template: `<input type="radio" value="foo" v-model="selected">`,
  82. }),
  83. ),
  84. ).toBe(`<input type="radio" value="foo" checked>`)
  85. expect(
  86. await renderToString(
  87. createApp({
  88. data: () => ({ selected: 'foo' }),
  89. template: `<input type="radio" value="bar" v-model="selected">`,
  90. }),
  91. ),
  92. ).toBe(`<input type="radio" value="bar">`)
  93. // non-string values
  94. expect(
  95. await renderToString(
  96. createApp({
  97. data: () => ({ selected: 'foo' }),
  98. template: `<input type="radio" :value="{}" v-model="selected">`,
  99. }),
  100. ),
  101. ).toBe(`<input type="radio">`)
  102. })
  103. test('select', async () => {
  104. expect(
  105. await renderToString(
  106. createApp({
  107. data: () => ({ model: 1 }),
  108. template: `<select v-model="model"><option value="0"></option><option value="1"></option></select>`,
  109. }),
  110. ),
  111. ).toBe(
  112. `<select><option value="0"></option><option value="1" selected></option></select>`,
  113. )
  114. expect(
  115. await renderToString(
  116. createApp({
  117. data: () => ({ model: [0, 1] }),
  118. template: `<select multiple v-model="model"><option value="0"></option><option value="1"></option></select>`,
  119. }),
  120. ),
  121. ).toBe(
  122. `<select multiple><option value="0" selected></option><option value="1" selected></option></select>`,
  123. )
  124. })
  125. test('checkbox', async () => {
  126. expect(
  127. await renderToString(
  128. createApp({
  129. data: () => ({ checked: true }),
  130. template: `<input type="checkbox" v-model="checked">`,
  131. }),
  132. ),
  133. ).toBe(`<input type="checkbox" checked>`)
  134. expect(
  135. await renderToString(
  136. createApp({
  137. data: () => ({ checked: false }),
  138. template: `<input type="checkbox" v-model="checked">`,
  139. }),
  140. ),
  141. ).toBe(`<input type="checkbox">`)
  142. expect(
  143. await renderToString(
  144. createApp({
  145. data: () => ({ checked: ['foo'] }),
  146. template: `<input type="checkbox" value="foo" v-model="checked">`,
  147. }),
  148. ),
  149. ).toBe(`<input type="checkbox" value="foo" checked>`)
  150. expect(
  151. await renderToString(
  152. createApp({
  153. data: () => ({ checked: [] }),
  154. template: `<input type="checkbox" value="foo" v-model="checked">`,
  155. }),
  156. ),
  157. ).toBe(`<input type="checkbox" value="foo">`)
  158. })
  159. test('textarea', async () => {
  160. expect(
  161. await renderToString(
  162. createApp({
  163. data: () => ({ foo: 'hello' }),
  164. template: `<textarea v-model="foo"/>`,
  165. }),
  166. ),
  167. ).toBe(`<textarea>hello</textarea>`)
  168. })
  169. test('dynamic type', async () => {
  170. expect(
  171. await renderToString(
  172. createApp({
  173. data: () => ({ type: 'text', model: 'hello' }),
  174. template: `<input :type="type" v-model="model">`,
  175. }),
  176. ),
  177. ).toBe(`<input type="text" value="hello">`)
  178. expect(
  179. await renderToString(
  180. createApp({
  181. data: () => ({ type: 'checkbox', model: true }),
  182. template: `<input :type="type" v-model="model">`,
  183. }),
  184. ),
  185. ).toBe(`<input type="checkbox" checked>`)
  186. expect(
  187. await renderToString(
  188. createApp({
  189. data: () => ({ type: 'checkbox', model: false }),
  190. template: `<input :type="type" v-model="model">`,
  191. }),
  192. ),
  193. ).toBe(`<input type="checkbox">`)
  194. expect(
  195. await renderToString(
  196. createApp({
  197. data: () => ({ type: 'checkbox', model: ['hello'] }),
  198. template: `<input :type="type" value="hello" v-model="model">`,
  199. }),
  200. ),
  201. ).toBe(`<input type="checkbox" value="hello" checked>`)
  202. expect(
  203. await renderToString(
  204. createApp({
  205. data: () => ({ type: 'checkbox', model: [] }),
  206. template: `<input :type="type" value="hello" v-model="model">`,
  207. }),
  208. ),
  209. ).toBe(`<input type="checkbox" value="hello">`)
  210. expect(
  211. await renderToString(
  212. createApp({
  213. data: () => ({ type: 'radio', model: 'hello' }),
  214. template: `<input :type="type" value="hello" v-model="model">`,
  215. }),
  216. ),
  217. ).toBe(`<input type="radio" value="hello" checked>`)
  218. expect(
  219. await renderToString(
  220. createApp({
  221. data: () => ({ type: 'radio', model: 'hello' }),
  222. template: `<input :type="type" value="bar" v-model="model">`,
  223. }),
  224. ),
  225. ).toBe(`<input type="radio" value="bar">`)
  226. })
  227. test('with v-bind', async () => {
  228. expect(
  229. await renderToString(
  230. createApp({
  231. data: () => ({
  232. obj: { type: 'radio', value: 'hello' },
  233. model: 'hello',
  234. }),
  235. template: `<input v-bind="obj" v-model="model">`,
  236. }),
  237. ),
  238. ).toBe(`<input type="radio" value="hello" checked>`)
  239. })
  240. })
  241. describe('vnode v-show', () => {
  242. test('basic', async () => {
  243. expect(
  244. await renderToString(
  245. createApp({
  246. render() {
  247. return withDirectives(h('div'), [[vShow, true]])
  248. },
  249. }),
  250. ),
  251. ).toBe(`<div></div>`)
  252. expect(
  253. await renderToString(
  254. createApp({
  255. render() {
  256. return withDirectives(h('div'), [[vShow, false]])
  257. },
  258. }),
  259. ),
  260. ).toBe(`<div style="display:none;"></div>`)
  261. })
  262. test('with merge', async () => {
  263. expect(
  264. await renderToString(
  265. createApp({
  266. render() {
  267. return withDirectives(
  268. h('div', {
  269. style: {
  270. color: 'red',
  271. },
  272. }),
  273. [[vShow, false]],
  274. )
  275. },
  276. }),
  277. ),
  278. ).toBe(`<div style="color:red;display:none;"></div>`)
  279. })
  280. })
  281. describe('vnode v-model', () => {
  282. test('text', async () => {
  283. expect(
  284. await renderToString(
  285. createApp({
  286. render() {
  287. return withDirectives(h('input'), [[vModelText, 'hello']])
  288. },
  289. }),
  290. ),
  291. ).toBe(`<input value="hello">`)
  292. })
  293. test('radio', async () => {
  294. expect(
  295. await renderToString(
  296. createApp({
  297. render() {
  298. return withDirectives(
  299. h('input', { type: 'radio', value: 'hello' }),
  300. [[vModelRadio, 'hello']],
  301. )
  302. },
  303. }),
  304. ),
  305. ).toBe(`<input type="radio" value="hello" checked>`)
  306. expect(
  307. await renderToString(
  308. createApp({
  309. render() {
  310. return withDirectives(
  311. h('input', { type: 'radio', value: 'hello' }),
  312. [[vModelRadio, 'foo']],
  313. )
  314. },
  315. }),
  316. ),
  317. ).toBe(`<input type="radio" value="hello">`)
  318. })
  319. test('checkbox', async () => {
  320. expect(
  321. await renderToString(
  322. createApp({
  323. render() {
  324. return withDirectives(h('input', { type: 'checkbox' }), [
  325. [vModelCheckbox, true],
  326. ])
  327. },
  328. }),
  329. ),
  330. ).toBe(`<input type="checkbox" checked>`)
  331. expect(
  332. await renderToString(
  333. createApp({
  334. render() {
  335. return withDirectives(h('input', { type: 'checkbox' }), [
  336. [vModelCheckbox, false],
  337. ])
  338. },
  339. }),
  340. ),
  341. ).toBe(`<input type="checkbox">`)
  342. expect(
  343. await renderToString(
  344. createApp({
  345. render() {
  346. return withDirectives(
  347. h('input', { type: 'checkbox', value: 'foo' }),
  348. [[vModelCheckbox, ['foo']]],
  349. )
  350. },
  351. }),
  352. ),
  353. ).toBe(`<input type="checkbox" value="foo" checked>`)
  354. expect(
  355. await renderToString(
  356. createApp({
  357. render() {
  358. return withDirectives(
  359. h('input', { type: 'checkbox', value: 'foo' }),
  360. [[vModelCheckbox, []]],
  361. )
  362. },
  363. }),
  364. ),
  365. ).toBe(`<input type="checkbox" value="foo">`)
  366. })
  367. })
  368. describe('vnode v-model dynamic', () => {
  369. test('text', async () => {
  370. expect(
  371. await renderToString(
  372. createApp({
  373. render() {
  374. return withDirectives(h('input'), [[vModelDynamic, 'hello']])
  375. },
  376. }),
  377. ),
  378. ).toBe(`<input value="hello">`)
  379. })
  380. test('radio', async () => {
  381. expect(
  382. await renderToString(
  383. createApp({
  384. render() {
  385. return withDirectives(
  386. h('input', { type: 'radio', value: 'hello' }),
  387. [[vModelDynamic, 'hello']],
  388. )
  389. },
  390. }),
  391. ),
  392. ).toBe(`<input type="radio" value="hello" checked>`)
  393. expect(
  394. await renderToString(
  395. createApp({
  396. render() {
  397. return withDirectives(
  398. h('input', { type: 'radio', value: 'hello' }),
  399. [[vModelDynamic, 'foo']],
  400. )
  401. },
  402. }),
  403. ),
  404. ).toBe(`<input type="radio" value="hello">`)
  405. })
  406. test('checkbox', async () => {
  407. expect(
  408. await renderToString(
  409. createApp({
  410. render() {
  411. return withDirectives(h('input', { type: 'checkbox' }), [
  412. [vModelDynamic, true],
  413. ])
  414. },
  415. }),
  416. ),
  417. ).toBe(`<input type="checkbox" checked>`)
  418. expect(
  419. await renderToString(
  420. createApp({
  421. render() {
  422. return withDirectives(h('input', { type: 'checkbox' }), [
  423. [vModelDynamic, false],
  424. ])
  425. },
  426. }),
  427. ),
  428. ).toBe(`<input type="checkbox">`)
  429. expect(
  430. await renderToString(
  431. createApp({
  432. render() {
  433. return withDirectives(
  434. h('input', { type: 'checkbox', value: 'foo' }),
  435. [[vModelDynamic, ['foo']]],
  436. )
  437. },
  438. }),
  439. ),
  440. ).toBe(`<input type="checkbox" value="foo" checked>`)
  441. expect(
  442. await renderToString(
  443. createApp({
  444. render() {
  445. return withDirectives(
  446. h('input', { type: 'checkbox', value: 'foo' }),
  447. [[vModelDynamic, []]],
  448. )
  449. },
  450. }),
  451. ),
  452. ).toBe(`<input type="checkbox" value="foo">`)
  453. })
  454. })
  455. test('custom directive w/ getSSRProps (vdom)', async () => {
  456. expect(
  457. await renderToString(
  458. createApp({
  459. render() {
  460. return withDirectives(h('div'), [
  461. [
  462. {
  463. getSSRProps({ value }) {
  464. return { id: value }
  465. },
  466. },
  467. 'foo',
  468. ],
  469. ])
  470. },
  471. }),
  472. ),
  473. ).toBe(`<div id="foo"></div>`)
  474. })
  475. test('custom directive w/ getSSRProps (optimized)', async () => {
  476. expect(
  477. await renderToString(
  478. createApp({
  479. data() {
  480. return {
  481. x: 'foo',
  482. }
  483. },
  484. directives: {
  485. xxx: {
  486. getSSRProps({ value, arg, modifiers }) {
  487. return { id: [value, arg, modifiers.ok].join('-') }
  488. },
  489. },
  490. },
  491. ssrRender(_ctx, _push, _parent, _attrs) {
  492. const _directive_xxx = resolveDirective('xxx')!
  493. _push(
  494. `<div${ssrRenderAttrs(
  495. ssrGetDirectiveProps(_ctx, _directive_xxx, _ctx.x, 'arg', {
  496. ok: true,
  497. }),
  498. )}></div>`,
  499. )
  500. },
  501. }),
  502. ),
  503. ).toBe(`<div id="foo-arg-true"></div>`)
  504. })
  505. // #7499
  506. test('custom directive w/ getSSRProps (expose)', async () => {
  507. let exposeVars: null | string | undefined = null
  508. const useTestDirective = () => ({
  509. vTest: {
  510. getSSRProps({ instance }: any) {
  511. if (instance) {
  512. exposeVars = instance.x
  513. }
  514. return { id: exposeVars }
  515. },
  516. },
  517. })
  518. const { vTest } = useTestDirective()
  519. const renderString = await renderToString(
  520. createApp({
  521. setup(props, { expose }) {
  522. const x = ref('foo')
  523. expose({ x })
  524. const __returned__ = { useTestDirective, vTest, ref, x }
  525. Object.defineProperty(__returned__, '__isScriptSetup', {
  526. enumerable: false,
  527. value: true,
  528. })
  529. return __returned__
  530. },
  531. ssrRender(_ctx, _push, _parent, _attrs) {
  532. _push(
  533. `<div${ssrRenderAttrs(
  534. mergeProps(_attrs!, ssrGetDirectiveProps(_ctx, unref(vTest))),
  535. )}></div>`,
  536. )
  537. },
  538. }),
  539. )
  540. expect(renderString).toBe(`<div id="foo"></div>`)
  541. expect(exposeVars).toBe('foo')
  542. })
  543. })