framework.spec.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722
  1. import * as framework from '../../../packages/weex-vue-framework'
  2. import { DEFAULT_ENV, Runtime, Instance } from 'weex-vdom-tester'
  3. import { config } from 'weex-js-runtime'
  4. import {
  5. syncPromise,
  6. checkRefresh
  7. } from '../helpers/index'
  8. let sendTasksHandler = () => {}
  9. const { Document, Element, Comment } = config
  10. function sendTasks () {
  11. sendTasksHandler.apply(null, arguments)
  12. }
  13. describe('framework APIs', () => {
  14. let runtime
  15. beforeEach(() => {
  16. Document.handler = sendTasks
  17. framework.init({ Document, Element, Comment, sendTasks })
  18. runtime = new Runtime(framework)
  19. sendTasksHandler = function () {
  20. runtime.target.callNative(...arguments)
  21. }
  22. })
  23. afterEach(() => {
  24. delete Document.handler
  25. framework.reset()
  26. })
  27. it('createInstance', () => {
  28. const instance = new Instance(runtime)
  29. framework.createInstance(instance.id, `
  30. new Vue({
  31. render: function (createElement) {
  32. return createElement('div', {}, [
  33. createElement('text', { attrs: { value: 'Hello' }}, [])
  34. ])
  35. },
  36. el: "body"
  37. })
  38. `)
  39. expect(instance.getRealRoot()).toEqual({
  40. type: 'div',
  41. children: [{ type: 'text', attr: { value: 'Hello' }}]
  42. })
  43. })
  44. it('createInstance with config', () => {
  45. const instance = new Instance(runtime)
  46. framework.createInstance(instance.id, `
  47. new Vue({
  48. render: function (createElement) {
  49. return createElement('div', {}, [
  50. createElement('text', { attrs: { value: JSON.stringify(this.$getConfig()) }}, [])
  51. ])
  52. },
  53. el: "body"
  54. })
  55. `, { bundleUrl: 'http://example.com/', a: 1, b: 2 })
  56. expect(instance.getRealRoot()).toEqual({
  57. type: 'div',
  58. children: [{ type: 'text', attr: { value: '{"bundleUrl":"http://example.com/","a":1,"b":2}' }}]
  59. })
  60. })
  61. it('createInstance with external data', () => {
  62. const instance = new Instance(runtime)
  63. framework.createInstance(instance.id, `
  64. new Vue({
  65. data: {
  66. a: 1,
  67. b: 2
  68. },
  69. render: function (createElement) {
  70. return createElement('div', {}, [
  71. createElement('text', { attrs: { value: this.a + '-' + this.b }}, [])
  72. ])
  73. },
  74. el: "body"
  75. })
  76. `, undefined, { a: 111 })
  77. expect(instance.getRealRoot()).toEqual({
  78. type: 'div',
  79. children: [{ type: 'text', attr: { value: '111-2' }}]
  80. })
  81. })
  82. it('destroyInstance', (done) => {
  83. const instance = new Instance(runtime)
  84. framework.createInstance(instance.id, `
  85. new Vue({
  86. data: {
  87. x: 'Hello'
  88. },
  89. render: function (createElement) {
  90. return createElement('div', {}, [
  91. createElement('text', { attrs: { value: this.x }}, [])
  92. ])
  93. },
  94. el: "body"
  95. })
  96. `)
  97. expect(instance.getRealRoot()).toEqual({
  98. type: 'div',
  99. children: [{ type: 'text', attr: { value: 'Hello' }}]
  100. })
  101. syncPromise([
  102. checkRefresh(instance, { x: 'World' }, result => {
  103. expect(result).toEqual({
  104. type: 'div',
  105. children: [{ type: 'text', attr: { value: 'World' }}]
  106. })
  107. framework.destroyInstance(instance.id)
  108. }),
  109. checkRefresh(instance, { x: 'Weex' }, result => {
  110. expect(result).toEqual({
  111. type: 'div',
  112. children: [{ type: 'text', attr: { value: 'World' }}]
  113. })
  114. done()
  115. })
  116. ])
  117. })
  118. it('refreshInstance', (done) => {
  119. const instance = new Instance(runtime)
  120. framework.createInstance(instance.id, `
  121. new Vue({
  122. data: {
  123. x: 'Hello'
  124. },
  125. render: function (createElement) {
  126. return createElement('div', {}, [
  127. createElement('text', { attrs: { value: this.x }}, [])
  128. ])
  129. },
  130. el: "body"
  131. })
  132. `)
  133. expect(instance.getRealRoot()).toEqual({
  134. type: 'div',
  135. children: [{ type: 'text', attr: { value: 'Hello' }}]
  136. })
  137. framework.refreshInstance(instance.id, { x: 'World' })
  138. setTimeout(() => {
  139. expect(instance.getRealRoot()).toEqual({
  140. type: 'div',
  141. children: [{ type: 'text', attr: { value: 'World' }}]
  142. })
  143. framework.destroyInstance(instance.id)
  144. const result = framework.refreshInstance(instance.id, { x: 'Weex' })
  145. expect(result instanceof Error).toBe(true)
  146. expect(result).toMatch(/refreshInstance/)
  147. expect(result).toMatch(/not found/)
  148. setTimeout(() => {
  149. expect(instance.getRealRoot()).toEqual({
  150. type: 'div',
  151. children: [{ type: 'text', attr: { value: 'World' }}]
  152. })
  153. done()
  154. })
  155. })
  156. })
  157. it('getRoot', () => {
  158. const instance = new Instance(runtime)
  159. framework.createInstance(instance.id, `
  160. new Vue({
  161. data: {
  162. x: 'Hello'
  163. },
  164. render: function (createElement) {
  165. return createElement('div', {}, [
  166. createElement('text', { attrs: { value: this.x }}, [])
  167. ])
  168. },
  169. el: "body"
  170. })
  171. `)
  172. let root = framework.getRoot(instance.id)
  173. expect(root.ref).toEqual('_root')
  174. expect(root.type).toEqual('div')
  175. expect(root.children.length).toEqual(1)
  176. expect(root.children[0].type).toEqual('text')
  177. expect(root.children[0].attr).toEqual({ value: 'Hello' })
  178. framework.destroyInstance(instance.id)
  179. root = framework.getRoot(instance.id)
  180. expect(root instanceof Error).toBe(true)
  181. expect(root).toMatch(/getRoot/)
  182. expect(root).toMatch(/not found/)
  183. })
  184. it('receiveTasks: fireEvent', (done) => {
  185. const instance = new Instance(runtime)
  186. framework.createInstance(instance.id, `
  187. new Vue({
  188. data: {
  189. x: 'Hello'
  190. },
  191. methods: {
  192. update: function (e) {
  193. this.x = 'World'
  194. }
  195. },
  196. render: function (createElement) {
  197. return createElement('div', {}, [
  198. createElement('text', { attrs: { value: this.x }, on: { click: this.update }}, [])
  199. ])
  200. },
  201. el: "body"
  202. })
  203. `)
  204. expect(instance.getRealRoot()).toEqual({
  205. type: 'div',
  206. children: [{
  207. type: 'text',
  208. attr: { value: 'Hello' },
  209. event: ['click']
  210. }]
  211. })
  212. const textRef = framework.getRoot(instance.id).children[0].ref
  213. framework.receiveTasks(instance.id, [
  214. { method: 'fireEvent', args: [textRef, 'click'] }
  215. ])
  216. setTimeout(() => {
  217. expect(instance.getRealRoot()).toEqual({
  218. type: 'div',
  219. children: [{
  220. type: 'text',
  221. attr: { value: 'World' },
  222. event: ['click']
  223. }]
  224. })
  225. framework.destroyInstance(instance.id)
  226. const result = framework.receiveTasks(instance.id, [
  227. { method: 'fireEvent', args: [textRef, 'click'] }
  228. ])
  229. expect(result instanceof Error).toBe(true)
  230. expect(result).toMatch(/invalid\sinstance\sid/)
  231. expect(result).toMatch(instance.id)
  232. done()
  233. })
  234. })
  235. it('receiveTasks: callback', (done) => {
  236. framework.registerModules({
  237. foo: ['a', 'b', 'c']
  238. })
  239. const instance = new Instance(runtime)
  240. framework.createInstance(instance.id, `
  241. const moduleFoo = weex.requireModule('foo')
  242. new Vue({
  243. data: {
  244. x: 'Hello'
  245. },
  246. methods: {
  247. update: function (data = {}) {
  248. this.x = data.value || 'World'
  249. }
  250. },
  251. mounted: function () {
  252. moduleFoo.a(data => {
  253. this.update(data)
  254. })
  255. },
  256. render: function (createElement) {
  257. return createElement('div', {}, [
  258. createElement('text', { attrs: { value: this.x }}, [])
  259. ])
  260. },
  261. el: "body"
  262. })
  263. `)
  264. expect(instance.getRealRoot()).toEqual({
  265. type: 'div',
  266. children: [{
  267. type: 'text',
  268. attr: { value: 'Hello' }
  269. }]
  270. })
  271. let callbackId
  272. instance.history.callNative.some(task => {
  273. if (task.module === 'foo' && task.method === 'a') {
  274. callbackId = task.args[0]
  275. return true
  276. }
  277. })
  278. framework.receiveTasks(instance.id, [
  279. { method: 'callback', args: [callbackId, undefined, true] }
  280. ])
  281. setTimeout(() => {
  282. expect(instance.getRealRoot()).toEqual({
  283. type: 'div',
  284. children: [{
  285. type: 'text',
  286. attr: { value: 'World' }
  287. }]
  288. })
  289. framework.receiveTasks(instance.id, [
  290. { method: 'callback', args: [callbackId, { value: 'Weex' }, true] }
  291. ])
  292. setTimeout(() => {
  293. expect(instance.getRealRoot()).toEqual({
  294. type: 'div',
  295. children: [{
  296. type: 'text',
  297. attr: { value: 'Weex' }
  298. }]
  299. })
  300. framework.receiveTasks(instance.id, [
  301. { method: 'callback', args: [callbackId] }
  302. ])
  303. setTimeout(() => {
  304. expect(instance.getRealRoot()).toEqual({
  305. type: 'div',
  306. children: [{
  307. type: 'text',
  308. attr: { value: 'World' }
  309. }]
  310. })
  311. framework.destroyInstance(instance.id)
  312. const result = framework.receiveTasks(instance.id, [
  313. { method: 'callback', args: [callbackId] }
  314. ])
  315. expect(result instanceof Error).toBe(true)
  316. expect(result).toMatch(/invalid\sinstance\sid/)
  317. expect(result).toMatch(instance.id)
  318. done()
  319. })
  320. })
  321. })
  322. })
  323. it('registerModules', () => {
  324. framework.registerModules({
  325. foo: ['a', 'b', 'c'],
  326. bar: [
  327. { name: 'a', args: ['string'] },
  328. { name: 'b', args: ['number'] },
  329. { name: 'c', args: ['string', 'number'] }
  330. ]
  331. })
  332. const instance = new Instance(runtime)
  333. framework.createInstance(instance.id, `
  334. const moduleFoo = weex.requireModule('foo')
  335. const moduleBar = weex.requireModule('bar')
  336. const moduleBaz = weex.requireModule('baz')
  337. new Vue({
  338. render: function (createElement) {
  339. const value = []
  340. if (typeof moduleFoo === 'object') {
  341. value.push('foo')
  342. value.push(Object.keys(moduleFoo))
  343. }
  344. if (typeof moduleBar === 'object') {
  345. value.push('bar')
  346. value.push(Object.keys(moduleBar))
  347. }
  348. if (typeof moduleBaz === 'object') {
  349. value.push('baz')
  350. value.push(Object.keys(moduleBaz))
  351. }
  352. return createElement('div', {}, [
  353. createElement('text', { attrs: { value: value.toString() }}, [])
  354. ])
  355. },
  356. mounted: function () {
  357. moduleFoo.a(1, '2', true)
  358. moduleBar.b(1)
  359. },
  360. el: "body"
  361. })
  362. `)
  363. expect(instance.getRealRoot()).toEqual({
  364. type: 'div',
  365. children: [{
  366. type: 'text',
  367. attr: { value: 'foo,a,b,c,bar,a,b,c,baz,' }
  368. }]
  369. })
  370. expect(instance.history.callNative
  371. .filter(task => task.module === 'foo')
  372. .map(task => `${task.method}(${task.args})`)
  373. ).toEqual(['a(1,2,true)'])
  374. expect(instance.history.callNative
  375. .filter(task => task.module === 'bar')
  376. .map(task => `${task.method}(${task.args})`)
  377. ).toEqual(['b(1)'])
  378. })
  379. it('isRegisteredModule', () => {
  380. framework.registerModules({
  381. foo: ['a', 'b'],
  382. bar: [
  383. { name: 'x', args: ['string'] },
  384. { name: 'y', args: ['number'] }
  385. ]
  386. })
  387. expect(framework.isRegisteredModule('foo')).toBe(true)
  388. expect(framework.isRegisteredModule('bar')).toBe(true)
  389. expect(framework.isRegisteredModule('foo', 'a')).toBe(true)
  390. expect(framework.isRegisteredModule('foo', 'b')).toBe(true)
  391. expect(framework.isRegisteredModule('bar', 'x')).toBe(true)
  392. expect(framework.isRegisteredModule('bar', 'y')).toBe(true)
  393. expect(framework.isRegisteredModule('FOO')).toBe(false)
  394. expect(framework.isRegisteredModule(' bar ')).toBe(false)
  395. expect(framework.isRegisteredModule('unknown')).toBe(false)
  396. expect(framework.isRegisteredModule('#}{)=}')).toBe(false)
  397. expect(framework.isRegisteredModule('foo', '')).toBe(false)
  398. expect(framework.isRegisteredModule('foo', 'c')).toBe(false)
  399. expect(framework.isRegisteredModule('bar', 'z')).toBe(false)
  400. expect(framework.isRegisteredModule('unknown', 'unknown')).toBe(false)
  401. })
  402. it('registerComponents', () => {
  403. framework.registerComponents(['foo', { type: 'bar' }, 'text'])
  404. const instance = new Instance(runtime)
  405. framework.createInstance(instance.id, `
  406. new Vue({
  407. render: function (createElement) {
  408. return createElement('div', {}, [
  409. createElement('text', {}, []),
  410. createElement('foo', {}, []),
  411. createElement('bar', {}, []),
  412. createElement('baz', {}, [])
  413. ])
  414. },
  415. el: "body"
  416. })
  417. `)
  418. expect(instance.getRealRoot()).toEqual({
  419. type: 'div',
  420. children: [{ type: 'text' }, { type: 'foo' }, { type: 'bar' }, { type: 'baz' }]
  421. })
  422. })
  423. it('isRegisteredComponent', () => {
  424. framework.registerComponents(['foo', { type: 'bar' }, 'text'])
  425. expect(framework.isRegisteredComponent('foo')).toBe(true)
  426. expect(framework.isRegisteredComponent('bar')).toBe(true)
  427. expect(framework.isRegisteredComponent('text')).toBe(true)
  428. expect(framework.isRegisteredComponent('FOO')).toBe(false)
  429. expect(framework.isRegisteredComponent(' bar ')).toBe(false)
  430. expect(framework.isRegisteredComponent('<text>')).toBe(false)
  431. expect(framework.isRegisteredComponent('#}{)=}')).toBe(false)
  432. })
  433. it('weex.supports', () => {
  434. framework.registerComponents(['apple', { type: 'banana' }])
  435. framework.registerModules({
  436. cat: ['eat', 'sleep'],
  437. dog: [
  438. { name: 'bark', args: ['string'] }
  439. ]
  440. })
  441. expect(framework.supports('@component/apple')).toBe(true)
  442. expect(framework.supports('@component/banana')).toBe(true)
  443. expect(framework.supports('@module/cat')).toBe(true)
  444. expect(framework.supports('@module/cat.eat')).toBe(true)
  445. expect(framework.supports('@module/cat.sleep')).toBe(true)
  446. expect(framework.supports('@module/dog.bark')).toBe(true)
  447. expect(framework.supports('@component/candy')).toBe(false)
  448. expect(framework.supports('@module/bird')).toBe(false)
  449. expect(framework.supports('@module/bird.sing')).toBe(false)
  450. expect(framework.supports('@module/dog.sleep')).toBe(false)
  451. expect(framework.supports('apple')).toBe(null)
  452. expect(framework.supports('<banana>')).toBe(null)
  453. expect(framework.supports('cat')).toBe(null)
  454. expect(framework.supports('@dog')).toBe(null)
  455. expect(framework.supports('@component/dog#bark')).toBe(null)
  456. })
  457. it('vm.$getConfig', () => {
  458. const instance = new Instance(runtime)
  459. instance.$create(`
  460. new Vue({
  461. render: function (createElement) {
  462. return createElement('div', {}, [
  463. createElement('text', { attrs: { value: JSON.stringify(this.$getConfig()) }}, [])
  464. ])
  465. },
  466. el: "body"
  467. })
  468. `)
  469. expect(JSON.parse(instance.getRealRoot().children[0].attr.value)).toEqual({ env: DEFAULT_ENV })
  470. const instance2 = new Instance(runtime)
  471. instance2.$create(`
  472. new Vue({
  473. render: function (createElement) {
  474. return createElement('div', {}, [
  475. createElement('text', { attrs: { value: JSON.stringify(this.$getConfig()) }}, [])
  476. ])
  477. },
  478. el: "body"
  479. })
  480. `, undefined, { a: 1, b: 2 })
  481. expect(JSON.parse(instance2.getRealRoot().children[0].attr.value)).toEqual({ a: 1, b: 2, env: DEFAULT_ENV })
  482. })
  483. it('Timer', (done) => {
  484. const instance = new Instance(runtime)
  485. instance.$create(`
  486. new Vue({
  487. data: {
  488. x: 0,
  489. y: 0
  490. },
  491. render: function (createElement) {
  492. return createElement('div', {}, [
  493. createElement('text', { attrs: { value: this.x + '-' + this.y }}, [])
  494. ])
  495. },
  496. mounted: function () {
  497. const now = Date.now()
  498. let timer, timer2
  499. setTimeout(() => {
  500. this.x = 1
  501. clearTimeout(timer)
  502. clearInterval(timer2)
  503. setInterval(() => {
  504. this.y++
  505. }, 600)
  506. }, 2000)
  507. timer = setTimeout(() => {
  508. this.x = 3
  509. }, 3000)
  510. setTimeout(() => {
  511. this.x = 3
  512. }, 4000)
  513. timer2 = setInterval(() => {
  514. this.y++
  515. }, 900)
  516. },
  517. el: "body"
  518. })
  519. `)
  520. expect(instance.getRealRoot()).toEqual({
  521. type: 'div',
  522. children: [{ type: 'text', attr: { value: '0-0' }}]
  523. })
  524. setTimeout(() => {
  525. expect(instance.getRealRoot().children[0].attr.value).toEqual('0-1')
  526. }, 950)
  527. setTimeout(() => {
  528. expect(instance.getRealRoot().children[0].attr.value).toEqual('0-2')
  529. }, 1850)
  530. setTimeout(() => {
  531. expect(instance.getRealRoot().children[0].attr.value).toEqual('1-2')
  532. }, 2050)
  533. setTimeout(() => {
  534. expect(instance.getRealRoot().children[0].attr.value).toEqual('1-3')
  535. }, 2650)
  536. setTimeout(() => {
  537. expect(instance.getRealRoot().children[0].attr.value).toEqual('1-4')
  538. }, 3250)
  539. setTimeout(() => {
  540. framework.destroyInstance(instance.id)
  541. }, 3500)
  542. setTimeout(() => {
  543. expect(instance.getRealRoot().children[0].attr.value).toEqual('1-4')
  544. done()
  545. }, 4100)
  546. })
  547. it('send function param', () => {
  548. framework.registerModules({
  549. foo: ['a']
  550. })
  551. const instance = new Instance(runtime)
  552. framework.createInstance(instance.id, `
  553. const moduleFoo = weex.requireModule('foo')
  554. new Vue({
  555. mounted: function () {
  556. moduleFoo.a(a => a + 1)
  557. },
  558. render: function (createElement) {
  559. return createElement('div', {}, [
  560. createElement('text', { attrs: { value: 'Hello' }}, [])
  561. ])
  562. },
  563. el: "body"
  564. })
  565. `)
  566. let callbackId
  567. instance.history.callNative.some(task => {
  568. if (task.module === 'foo' && task.method === 'a') {
  569. callbackId = task.args[0]
  570. return true
  571. }
  572. })
  573. expect(typeof callbackId).toEqual('string')
  574. })
  575. it('send Element param', () => {
  576. framework.registerModules({
  577. foo: ['a']
  578. })
  579. const instance = new Instance(runtime)
  580. framework.createInstance(instance.id, `
  581. const moduleFoo = weex.requireModule('foo')
  582. new Vue({
  583. mounted: function () {
  584. moduleFoo.a(this.$refs.x)
  585. },
  586. render: function (createElement) {
  587. return createElement('div', {}, [
  588. createElement('text', { attrs: { value: 'Hello' }, ref: 'x' }, [])
  589. ])
  590. },
  591. el: "body"
  592. })
  593. `)
  594. let callbackId
  595. instance.history.callNative.some(task => {
  596. if (task.module === 'foo' && task.method === 'a') {
  597. callbackId = task.args[0]
  598. return true
  599. }
  600. })
  601. expect(typeof callbackId).toEqual('string')
  602. })
  603. it('registering global assets', () => {
  604. const instance = new Instance(runtime)
  605. framework.createInstance(instance.id, `
  606. Vue.component('test', {
  607. render (h) {
  608. return h('div', 'Hello')
  609. }
  610. })
  611. new Vue({
  612. render (h) {
  613. return h('test')
  614. },
  615. el: 'body'
  616. })
  617. `)
  618. expect(instance.getRealRoot()).toEqual({
  619. type: 'div',
  620. children: [{ type: 'text', attr: { value: 'Hello' }}]
  621. })
  622. })
  623. it('adding prototype methods', () => {
  624. const instance = new Instance(runtime)
  625. framework.createInstance(instance.id, `
  626. Vue.prototype.$test = () => 'Hello'
  627. const Test = {
  628. render (h) {
  629. return h('div', this.$test())
  630. }
  631. }
  632. new Vue({
  633. render (h) {
  634. return h(Test)
  635. },
  636. el: 'body'
  637. })
  638. `)
  639. expect(instance.getRealRoot()).toEqual({
  640. type: 'div',
  641. children: [{ type: 'text', attr: { value: 'Hello' }}]
  642. })
  643. })
  644. it('using global mixins', () => {
  645. const instance = new Instance(runtime)
  646. framework.createInstance(instance.id, `
  647. Vue.mixin({
  648. created () {
  649. this.test = true
  650. }
  651. })
  652. const Test = {
  653. data: () => ({ test: false }),
  654. render (h) {
  655. return h('div', this.test ? 'Hello' : 'nope')
  656. }
  657. }
  658. new Vue({
  659. data: { test: false },
  660. render (h) {
  661. return this.test ? h(Test) : h('p')
  662. },
  663. el: 'body'
  664. })
  665. `)
  666. expect(instance.getRealRoot()).toEqual({
  667. type: 'div',
  668. children: [{ type: 'text', attr: { value: 'Hello' }}]
  669. })
  670. })
  671. })