ssrDirectives.spec.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623
  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('template with v-text / v-html', () => {
  242. test('element with v-html', async () => {
  243. expect(
  244. await renderToString(
  245. createApp({
  246. data: () => ({ foo: 'hello' }),
  247. template: `<span v-html="foo"/>`,
  248. }),
  249. ),
  250. ).toBe(`<span>hello</span>`)
  251. })
  252. test('textarea with v-text', async () => {
  253. expect(
  254. await renderToString(
  255. createApp({
  256. data: () => ({ foo: 'hello' }),
  257. template: `<textarea v-text="foo"/>`,
  258. }),
  259. ),
  260. ).toBe(`<textarea>hello</textarea>`)
  261. })
  262. test('textarea with v-html', async () => {
  263. expect(
  264. await renderToString(
  265. createApp({
  266. data: () => ({ foo: 'hello' }),
  267. template: `<textarea v-html="foo"/>`,
  268. }),
  269. ),
  270. ).toBe(`<textarea>hello</textarea>`)
  271. })
  272. })
  273. describe('vnode v-show', () => {
  274. test('basic', async () => {
  275. expect(
  276. await renderToString(
  277. createApp({
  278. render() {
  279. return withDirectives(h('div'), [[vShow, true]])
  280. },
  281. }),
  282. ),
  283. ).toBe(`<div></div>`)
  284. expect(
  285. await renderToString(
  286. createApp({
  287. render() {
  288. return withDirectives(h('div'), [[vShow, false]])
  289. },
  290. }),
  291. ),
  292. ).toBe(`<div style="display:none;"></div>`)
  293. })
  294. test('with merge', async () => {
  295. expect(
  296. await renderToString(
  297. createApp({
  298. render() {
  299. return withDirectives(
  300. h('div', {
  301. style: {
  302. color: 'red',
  303. },
  304. }),
  305. [[vShow, false]],
  306. )
  307. },
  308. }),
  309. ),
  310. ).toBe(`<div style="color:red;display:none;"></div>`)
  311. })
  312. })
  313. describe('vnode v-model', () => {
  314. test('text', async () => {
  315. expect(
  316. await renderToString(
  317. createApp({
  318. render() {
  319. return withDirectives(h('input'), [[vModelText, 'hello']])
  320. },
  321. }),
  322. ),
  323. ).toBe(`<input value="hello">`)
  324. })
  325. test('radio', async () => {
  326. expect(
  327. await renderToString(
  328. createApp({
  329. render() {
  330. return withDirectives(
  331. h('input', { type: 'radio', value: 'hello' }),
  332. [[vModelRadio, 'hello']],
  333. )
  334. },
  335. }),
  336. ),
  337. ).toBe(`<input type="radio" value="hello" checked>`)
  338. expect(
  339. await renderToString(
  340. createApp({
  341. render() {
  342. return withDirectives(
  343. h('input', { type: 'radio', value: 'hello' }),
  344. [[vModelRadio, 'foo']],
  345. )
  346. },
  347. }),
  348. ),
  349. ).toBe(`<input type="radio" value="hello">`)
  350. })
  351. test('checkbox', async () => {
  352. expect(
  353. await renderToString(
  354. createApp({
  355. render() {
  356. return withDirectives(h('input', { type: 'checkbox' }), [
  357. [vModelCheckbox, true],
  358. ])
  359. },
  360. }),
  361. ),
  362. ).toBe(`<input type="checkbox" checked>`)
  363. expect(
  364. await renderToString(
  365. createApp({
  366. render() {
  367. return withDirectives(h('input', { type: 'checkbox' }), [
  368. [vModelCheckbox, false],
  369. ])
  370. },
  371. }),
  372. ),
  373. ).toBe(`<input type="checkbox">`)
  374. expect(
  375. await renderToString(
  376. createApp({
  377. render() {
  378. return withDirectives(
  379. h('input', { type: 'checkbox', value: 'foo' }),
  380. [[vModelCheckbox, ['foo']]],
  381. )
  382. },
  383. }),
  384. ),
  385. ).toBe(`<input type="checkbox" value="foo" checked>`)
  386. expect(
  387. await renderToString(
  388. createApp({
  389. render() {
  390. return withDirectives(
  391. h('input', { type: 'checkbox', value: 'foo' }),
  392. [[vModelCheckbox, []]],
  393. )
  394. },
  395. }),
  396. ),
  397. ).toBe(`<input type="checkbox" value="foo">`)
  398. })
  399. })
  400. describe('vnode v-model dynamic', () => {
  401. test('text', async () => {
  402. expect(
  403. await renderToString(
  404. createApp({
  405. render() {
  406. return withDirectives(h('input'), [[vModelDynamic, 'hello']])
  407. },
  408. }),
  409. ),
  410. ).toBe(`<input value="hello">`)
  411. })
  412. test('radio', async () => {
  413. expect(
  414. await renderToString(
  415. createApp({
  416. render() {
  417. return withDirectives(
  418. h('input', { type: 'radio', value: 'hello' }),
  419. [[vModelDynamic, 'hello']],
  420. )
  421. },
  422. }),
  423. ),
  424. ).toBe(`<input type="radio" value="hello" checked>`)
  425. expect(
  426. await renderToString(
  427. createApp({
  428. render() {
  429. return withDirectives(
  430. h('input', { type: 'radio', value: 'hello' }),
  431. [[vModelDynamic, 'foo']],
  432. )
  433. },
  434. }),
  435. ),
  436. ).toBe(`<input type="radio" value="hello">`)
  437. })
  438. test('checkbox', async () => {
  439. expect(
  440. await renderToString(
  441. createApp({
  442. render() {
  443. return withDirectives(h('input', { type: 'checkbox' }), [
  444. [vModelDynamic, true],
  445. ])
  446. },
  447. }),
  448. ),
  449. ).toBe(`<input type="checkbox" checked>`)
  450. expect(
  451. await renderToString(
  452. createApp({
  453. render() {
  454. return withDirectives(h('input', { type: 'checkbox' }), [
  455. [vModelDynamic, false],
  456. ])
  457. },
  458. }),
  459. ),
  460. ).toBe(`<input type="checkbox">`)
  461. expect(
  462. await renderToString(
  463. createApp({
  464. render() {
  465. return withDirectives(
  466. h('input', { type: 'checkbox', value: 'foo' }),
  467. [[vModelDynamic, ['foo']]],
  468. )
  469. },
  470. }),
  471. ),
  472. ).toBe(`<input type="checkbox" value="foo" checked>`)
  473. expect(
  474. await renderToString(
  475. createApp({
  476. render() {
  477. return withDirectives(
  478. h('input', { type: 'checkbox', value: 'foo' }),
  479. [[vModelDynamic, []]],
  480. )
  481. },
  482. }),
  483. ),
  484. ).toBe(`<input type="checkbox" value="foo">`)
  485. })
  486. })
  487. test('custom directive w/ getSSRProps (vdom)', async () => {
  488. expect(
  489. await renderToString(
  490. createApp({
  491. render() {
  492. return withDirectives(h('div'), [
  493. [
  494. {
  495. getSSRProps({ value }) {
  496. return { id: value }
  497. },
  498. },
  499. 'foo',
  500. ],
  501. ])
  502. },
  503. }),
  504. ),
  505. ).toBe(`<div id="foo"></div>`)
  506. })
  507. test('custom directive w/ getSSRProps (optimized)', async () => {
  508. expect(
  509. await renderToString(
  510. createApp({
  511. data() {
  512. return {
  513. x: 'foo',
  514. }
  515. },
  516. directives: {
  517. xxx: {
  518. getSSRProps({ value, arg, modifiers }) {
  519. return { id: [value, arg, modifiers.ok].join('-') }
  520. },
  521. },
  522. },
  523. ssrRender(_ctx, _push, _parent, _attrs) {
  524. const _directive_xxx = resolveDirective('xxx')!
  525. _push(
  526. `<div${ssrRenderAttrs(
  527. ssrGetDirectiveProps(_ctx, _directive_xxx, _ctx.x, 'arg', {
  528. ok: true,
  529. }),
  530. )}></div>`,
  531. )
  532. },
  533. }),
  534. ),
  535. ).toBe(`<div id="foo-arg-true"></div>`)
  536. })
  537. // #7499
  538. test('custom directive w/ getSSRProps (expose)', async () => {
  539. let exposeVars: null | string | undefined = null
  540. const useTestDirective = () => ({
  541. vTest: {
  542. getSSRProps({ instance }: any) {
  543. if (instance) {
  544. exposeVars = instance.x
  545. }
  546. return { id: exposeVars }
  547. },
  548. },
  549. })
  550. const { vTest } = useTestDirective()
  551. const renderString = await renderToString(
  552. createApp({
  553. setup(props, { expose }) {
  554. const x = ref('foo')
  555. expose({ x })
  556. const __returned__ = { useTestDirective, vTest, ref, x }
  557. Object.defineProperty(__returned__, '__isScriptSetup', {
  558. enumerable: false,
  559. value: true,
  560. })
  561. return __returned__
  562. },
  563. ssrRender(_ctx, _push, _parent, _attrs) {
  564. _push(
  565. `<div${ssrRenderAttrs(
  566. mergeProps(_attrs!, ssrGetDirectiveProps(_ctx, unref(vTest))),
  567. )}></div>`,
  568. )
  569. },
  570. }),
  571. )
  572. expect(renderString).toBe(`<div id="foo"></div>`)
  573. expect(exposeVars).toBe('foo')
  574. })
  575. })