modules.spec.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865
  1. import { nextTick } from 'vue'
  2. import Vuex from '@/index'
  3. const TEST = 'TEST'
  4. describe('Modules', () => {
  5. describe('module registration', () => {
  6. it('dynamic module registration', () => {
  7. const store = new Vuex.Store({
  8. strict: true,
  9. modules: {
  10. foo: {
  11. state: { bar: 1 },
  12. mutations: { inc: state => state.bar++ },
  13. actions: { incFoo: ({ commit }) => commit('inc') },
  14. getters: { bar: state => state.bar }
  15. }
  16. }
  17. })
  18. expect(() => {
  19. store.registerModule('hi', {
  20. state: { a: 1 },
  21. mutations: { inc: state => state.a++ },
  22. actions: { inc: ({ commit }) => commit('inc') },
  23. getters: { a: state => state.a }
  24. })
  25. }).not.toThrow()
  26. expect(store._mutations.inc.length).toBe(2)
  27. expect(store.state.hi.a).toBe(1)
  28. expect(store.getters.a).toBe(1)
  29. // assert initial modules work as expected after dynamic registration
  30. expect(store.state.foo.bar).toBe(1)
  31. expect(store.getters.bar).toBe(1)
  32. // test dispatching actions defined in dynamic module
  33. store.dispatch('inc')
  34. expect(store.state.hi.a).toBe(2)
  35. expect(store.getters.a).toBe(2)
  36. expect(store.state.foo.bar).toBe(2)
  37. expect(store.getters.bar).toBe(2)
  38. // unregister
  39. store.unregisterModule('hi')
  40. expect(store.state.hi).toBeUndefined()
  41. expect(store.getters.a).toBeUndefined()
  42. expect(store._mutations.inc.length).toBe(1)
  43. expect(store._actions.inc).toBeUndefined()
  44. // assert initial modules still work as expected after unregister
  45. store.dispatch('incFoo')
  46. expect(store.state.foo.bar).toBe(3)
  47. expect(store.getters.bar).toBe(3)
  48. })
  49. it('dynamic module registration with namespace inheritance', () => {
  50. const store = new Vuex.Store({
  51. modules: {
  52. a: {
  53. namespaced: true
  54. }
  55. }
  56. })
  57. const actionSpy = jest.fn()
  58. const mutationSpy = jest.fn()
  59. store.registerModule(['a', 'b'], {
  60. state: { value: 1 },
  61. getters: { foo: state => state.value },
  62. actions: { foo: actionSpy },
  63. mutations: { foo: mutationSpy }
  64. })
  65. expect(store.state.a.b.value).toBe(1)
  66. expect(store.getters['a/foo']).toBe(1)
  67. store.dispatch('a/foo')
  68. expect(actionSpy).toHaveBeenCalled()
  69. store.commit('a/foo')
  70. expect(mutationSpy).toHaveBeenCalled()
  71. })
  72. it('dynamic module existance test', () => {
  73. const store = new Vuex.Store({})
  74. store.registerModule('bonjour', {})
  75. expect(store.hasModule('bonjour')).toBe(true)
  76. store.unregisterModule('bonjour')
  77. expect(store.hasModule('bonjour')).toBe(false)
  78. })
  79. it('dynamic module registration preserving hydration', () => {
  80. const store = new Vuex.Store({})
  81. store.replaceState({ a: { foo: 'state' }})
  82. const actionSpy = jest.fn()
  83. const mutationSpy = jest.fn()
  84. store.registerModule('a', {
  85. namespaced: true,
  86. getters: { foo: state => state.foo },
  87. actions: { foo: actionSpy },
  88. mutations: { foo: mutationSpy }
  89. }, { preserveState: true })
  90. expect(store.state.a.foo).toBe('state')
  91. expect(store.getters['a/foo']).toBe('state')
  92. store.dispatch('a/foo')
  93. expect(actionSpy).toHaveBeenCalled()
  94. store.commit('a/foo')
  95. expect(mutationSpy).toHaveBeenCalled()
  96. })
  97. })
  98. // #524
  99. it('should not fire an unrelated watcher', done => {
  100. const spy = jest.fn()
  101. const store = new Vuex.Store({
  102. modules: {
  103. a: {
  104. state: { value: 1 }
  105. },
  106. b: {}
  107. }
  108. })
  109. store.watch(state => state.a, spy)
  110. store.registerModule(['b', 'c'], {
  111. state: { value: 2 }
  112. })
  113. nextTick(() => {
  114. expect(spy).not.toHaveBeenCalled()
  115. done()
  116. })
  117. })
  118. describe('modules usage', () => {
  119. it('state as function (multiple module in same store)', () => {
  120. const module = {
  121. state () {
  122. return { a: 0 }
  123. },
  124. mutations: {
  125. [TEST] (state, n) {
  126. state.a += n
  127. }
  128. }
  129. }
  130. const store = new Vuex.Store({
  131. modules: {
  132. one: module,
  133. two: module
  134. }
  135. })
  136. expect(store.state.one.a).toBe(0)
  137. expect(store.state.two.a).toBe(0)
  138. store.commit(TEST, 1)
  139. expect(store.state.one.a).toBe(1)
  140. expect(store.state.two.a).toBe(1)
  141. })
  142. it('state as function (same module in multiple stores)', () => {
  143. const module = {
  144. state () {
  145. return { a: 0 }
  146. },
  147. mutations: {
  148. [TEST] (state, n) {
  149. state.a += n
  150. }
  151. }
  152. }
  153. const storeA = new Vuex.Store({
  154. modules: {
  155. foo: module
  156. }
  157. })
  158. const storeB = new Vuex.Store({
  159. modules: {
  160. bar: module
  161. }
  162. })
  163. expect(storeA.state.foo.a).toBe(0)
  164. expect(storeB.state.bar.a).toBe(0)
  165. storeA.commit(TEST, 1)
  166. expect(storeA.state.foo.a).toBe(1)
  167. expect(storeB.state.bar.a).toBe(0)
  168. storeB.commit(TEST, 2)
  169. expect(storeA.state.foo.a).toBe(1)
  170. expect(storeB.state.bar.a).toBe(2)
  171. })
  172. it('module: mutation', function () {
  173. const mutations = {
  174. [TEST] (state, n) {
  175. state.a += n
  176. }
  177. }
  178. const store = new Vuex.Store({
  179. state: {
  180. a: 1
  181. },
  182. mutations,
  183. modules: {
  184. nested: {
  185. state: { a: 2 },
  186. mutations,
  187. modules: {
  188. one: {
  189. state: { a: 3 },
  190. mutations
  191. },
  192. nested: {
  193. modules: {
  194. two: {
  195. state: { a: 4 },
  196. mutations
  197. },
  198. three: {
  199. state: { a: 5 },
  200. mutations
  201. }
  202. }
  203. }
  204. }
  205. },
  206. four: {
  207. state: { a: 6 },
  208. mutations
  209. }
  210. }
  211. })
  212. store.commit(TEST, 1)
  213. expect(store.state.a).toBe(2)
  214. expect(store.state.nested.a).toBe(3)
  215. expect(store.state.nested.one.a).toBe(4)
  216. expect(store.state.nested.nested.two.a).toBe(5)
  217. expect(store.state.nested.nested.three.a).toBe(6)
  218. expect(store.state.four.a).toBe(7)
  219. })
  220. it('module: action', function () {
  221. let calls = 0
  222. const makeAction = n => {
  223. return {
  224. [TEST] ({ state, rootState }) {
  225. calls++
  226. expect(state.a).toBe(n)
  227. expect(rootState).toBe(store.state)
  228. }
  229. }
  230. }
  231. const store = new Vuex.Store({
  232. state: {
  233. a: 1
  234. },
  235. actions: makeAction(1),
  236. modules: {
  237. nested: {
  238. state: { a: 2 },
  239. actions: makeAction(2),
  240. modules: {
  241. one: {
  242. state: { a: 3 },
  243. actions: makeAction(3)
  244. },
  245. nested: {
  246. modules: {
  247. two: {
  248. state: { a: 4 },
  249. actions: makeAction(4)
  250. },
  251. three: {
  252. state: { a: 5 },
  253. actions: makeAction(5)
  254. }
  255. }
  256. }
  257. }
  258. },
  259. four: {
  260. state: { a: 6 },
  261. actions: makeAction(6)
  262. }
  263. }
  264. })
  265. store.dispatch(TEST)
  266. expect(calls).toBe(6)
  267. })
  268. it('module: getters', function () {
  269. const makeGetter = n => ({
  270. [`getter${n}`]: (state, getters, rootState) => {
  271. expect(getters.constant).toBe(0)
  272. expect(rootState).toBe(store.state)
  273. return state.a
  274. }
  275. })
  276. const store = new Vuex.Store({
  277. state: {
  278. a: 1
  279. },
  280. getters: {
  281. constant: () => 0,
  282. ...makeGetter(1)
  283. },
  284. modules: {
  285. nested: {
  286. state: { a: 2 },
  287. getters: makeGetter(2),
  288. modules: {
  289. one: {
  290. state: { a: 3 },
  291. getters: makeGetter(3)
  292. },
  293. nested: {
  294. modules: {
  295. two: {
  296. state: { a: 4 },
  297. getters: makeGetter(4)
  298. },
  299. three: {
  300. state: { a: 5 },
  301. getters: makeGetter(5)
  302. }
  303. }
  304. }
  305. }
  306. },
  307. four: {
  308. state: { a: 6 },
  309. getters: makeGetter(6)
  310. }
  311. }
  312. })
  313. ;[1, 2, 3, 4, 5, 6].forEach(n => {
  314. expect(store.getters[`getter${n}`]).toBe(n)
  315. })
  316. })
  317. it('module: namespace', () => {
  318. const actionSpy = jest.fn()
  319. const mutationSpy = jest.fn()
  320. const store = new Vuex.Store({
  321. modules: {
  322. a: {
  323. namespaced: true,
  324. state: {
  325. a: 1
  326. },
  327. getters: {
  328. b: () => 2
  329. },
  330. actions: {
  331. [TEST]: actionSpy
  332. },
  333. mutations: {
  334. [TEST]: mutationSpy
  335. }
  336. }
  337. }
  338. })
  339. expect(store.state.a.a).toBe(1)
  340. expect(store.getters['a/b']).toBe(2)
  341. store.dispatch('a/' + TEST)
  342. expect(actionSpy).toHaveBeenCalled()
  343. store.commit('a/' + TEST)
  344. expect(mutationSpy).toHaveBeenCalled()
  345. })
  346. it('module: nested namespace', () => {
  347. // mock module generator
  348. const actionSpys = []
  349. const mutationSpys = []
  350. const createModule = (name, namespaced, children) => {
  351. const actionSpy = jest.fn()
  352. const mutationSpy = jest.fn()
  353. actionSpys.push(actionSpy)
  354. mutationSpys.push(mutationSpy)
  355. return {
  356. namespaced,
  357. state: {
  358. [name]: true
  359. },
  360. getters: {
  361. [name]: state => state[name]
  362. },
  363. actions: {
  364. [name]: actionSpy
  365. },
  366. mutations: {
  367. [name]: mutationSpy
  368. },
  369. modules: children
  370. }
  371. }
  372. // mock module
  373. const modules = {
  374. a: createModule('a', true, { // a/a
  375. b: createModule('b', false, { // a/b - does not add namespace
  376. c: createModule('c', true) // a/c/c
  377. }),
  378. d: createModule('d', true) // a/d/d
  379. })
  380. }
  381. const store = new Vuex.Store({ modules })
  382. const expectedTypes = [
  383. 'a/a', 'a/b', 'a/c/c', 'a/d/d'
  384. ]
  385. // getters
  386. expectedTypes.forEach(type => {
  387. expect(store.getters[type]).toBe(true)
  388. })
  389. // actions
  390. expectedTypes.forEach(type => {
  391. store.dispatch(type)
  392. })
  393. actionSpys.forEach(spy => {
  394. expect(spy).toHaveBeenCalledTimes(1)
  395. })
  396. // mutations
  397. expectedTypes.forEach(type => {
  398. store.commit(type)
  399. })
  400. mutationSpys.forEach(spy => {
  401. expect(spy).toHaveBeenCalledTimes(1)
  402. })
  403. })
  404. it('module: getters are namespaced in namespaced module', () => {
  405. const store = new Vuex.Store({
  406. state: { value: 'root' },
  407. getters: {
  408. foo: state => state.value
  409. },
  410. modules: {
  411. a: {
  412. namespaced: true,
  413. state: { value: 'module' },
  414. getters: {
  415. foo: state => state.value,
  416. bar: (state, getters) => getters.foo,
  417. baz: (state, getters, rootState, rootGetters) => rootGetters.foo
  418. }
  419. }
  420. }
  421. })
  422. expect(store.getters['a/foo']).toBe('module')
  423. expect(store.getters['a/bar']).toBe('module')
  424. expect(store.getters['a/baz']).toBe('root')
  425. })
  426. it('module: action context is namespaced in namespaced module', done => {
  427. const rootActionSpy = jest.fn()
  428. const rootMutationSpy = jest.fn()
  429. const moduleActionSpy = jest.fn()
  430. const moduleMutationSpy = jest.fn()
  431. const store = new Vuex.Store({
  432. state: { value: 'root' },
  433. getters: { foo: state => state.value },
  434. actions: { foo: rootActionSpy },
  435. mutations: { foo: rootMutationSpy },
  436. modules: {
  437. a: {
  438. namespaced: true,
  439. state: { value: 'module' },
  440. getters: { foo: state => state.value },
  441. actions: {
  442. foo: moduleActionSpy,
  443. test ({ dispatch, commit, getters, rootGetters }) {
  444. expect(getters.foo).toBe('module')
  445. expect(rootGetters.foo).toBe('root')
  446. dispatch('foo')
  447. expect(moduleActionSpy).toHaveBeenCalledTimes(1)
  448. dispatch('foo', null, { root: true })
  449. expect(rootActionSpy).toHaveBeenCalledTimes(1)
  450. commit('foo')
  451. expect(moduleMutationSpy).toHaveBeenCalledTimes(1)
  452. commit('foo', null, { root: true })
  453. expect(rootMutationSpy).toHaveBeenCalledTimes(1)
  454. done()
  455. }
  456. },
  457. mutations: { foo: moduleMutationSpy }
  458. }
  459. }
  460. })
  461. store.dispatch('a/test')
  462. })
  463. it('module: use other module that has same namespace', done => {
  464. const actionSpy = jest.fn()
  465. const mutationSpy = jest.fn()
  466. const store = new Vuex.Store({
  467. modules: {
  468. parent: {
  469. namespaced: true,
  470. modules: {
  471. a: {
  472. state: { value: 'a' },
  473. getters: { foo: state => state.value },
  474. actions: { foo: actionSpy },
  475. mutations: { foo: mutationSpy }
  476. },
  477. b: {
  478. state: { value: 'b' },
  479. getters: { bar: (state, getters) => getters.foo },
  480. actions: {
  481. test ({ dispatch, commit, getters }) {
  482. expect(getters.foo).toBe('a')
  483. expect(getters.bar).toBe('a')
  484. dispatch('foo')
  485. expect(actionSpy).toHaveBeenCalled()
  486. commit('foo')
  487. expect(mutationSpy).toHaveBeenCalled()
  488. done()
  489. }
  490. }
  491. }
  492. }
  493. }
  494. }
  495. })
  496. store.dispatch('parent/test')
  497. })
  498. it('module: warn when module overrides state', () => {
  499. jest.spyOn(console, 'warn').mockImplementation()
  500. const store = new Vuex.Store({
  501. modules: {
  502. foo: {
  503. state () {
  504. return { value: 1 }
  505. },
  506. modules: {
  507. value: {
  508. state: () => 2
  509. }
  510. }
  511. }
  512. }
  513. })
  514. expect(store.state.foo.value).toBe(2)
  515. expect(console.warn).toHaveBeenCalledWith(
  516. `[vuex] state field "value" was overridden by a module with the same name at "foo.value"`
  517. )
  518. })
  519. it('dispatching multiple actions in different modules', done => {
  520. const store = new Vuex.Store({
  521. modules: {
  522. a: {
  523. actions: {
  524. [TEST] () {
  525. return 1
  526. }
  527. }
  528. },
  529. b: {
  530. actions: {
  531. [TEST] () {
  532. return new Promise(r => r(2))
  533. }
  534. }
  535. }
  536. }
  537. })
  538. store.dispatch(TEST).then(res => {
  539. expect(res[0]).toBe(1)
  540. expect(res[1]).toBe(2)
  541. done()
  542. })
  543. })
  544. it('root actions dispatched in namespaced modules', done => {
  545. const store = new Vuex.Store({
  546. modules: {
  547. a: {
  548. namespaced: true,
  549. actions: {
  550. [TEST]: {
  551. root: true,
  552. handler () {
  553. return 1
  554. }
  555. }
  556. }
  557. },
  558. b: {
  559. namespaced: true,
  560. actions: {
  561. [TEST]: {
  562. root: true,
  563. handler () {
  564. return new Promise(r => r(2))
  565. }
  566. }
  567. }
  568. },
  569. c: {
  570. namespaced: true,
  571. actions: {
  572. [TEST]: {
  573. handler () {
  574. // Should not be called
  575. return 3
  576. }
  577. }
  578. }
  579. },
  580. d: {
  581. namespaced: true,
  582. actions: {
  583. [TEST] () {
  584. // Should not be called
  585. return 4
  586. }
  587. }
  588. }
  589. }
  590. })
  591. store.dispatch(TEST).then(res => {
  592. expect(res.length).toBe(2)
  593. expect(res[0]).toBe(1)
  594. expect(res[1]).toBe(2)
  595. done()
  596. })
  597. })
  598. it('plugins', function () {
  599. let initState
  600. const actionSpy = jest.fn()
  601. const mutations = []
  602. const subscribeActionSpy = jest.fn()
  603. const store = new Vuex.Store({
  604. state: {
  605. a: 1
  606. },
  607. mutations: {
  608. [TEST] (state, n) {
  609. state.a += n
  610. }
  611. },
  612. actions: {
  613. [TEST]: actionSpy
  614. },
  615. plugins: [
  616. store => {
  617. initState = store.state
  618. store.subscribe((mut, state) => {
  619. expect(state).toBe(state)
  620. mutations.push(mut)
  621. })
  622. store.subscribeAction(subscribeActionSpy)
  623. }
  624. ]
  625. })
  626. expect(initState).toBe(store.state)
  627. store.commit(TEST, 2)
  628. store.dispatch(TEST, 2)
  629. expect(mutations.length).toBe(1)
  630. expect(mutations[0].type).toBe(TEST)
  631. expect(mutations[0].payload).toBe(2)
  632. expect(actionSpy).toHaveBeenCalled()
  633. expect(subscribeActionSpy).toHaveBeenCalledWith(
  634. { type: TEST, payload: 2 },
  635. store.state
  636. )
  637. })
  638. it('action before/after subscribers', (done) => {
  639. const beforeSpy = jest.fn()
  640. const afterSpy = jest.fn()
  641. const store = new Vuex.Store({
  642. actions: {
  643. [TEST]: () => Promise.resolve()
  644. },
  645. plugins: [
  646. store => {
  647. store.subscribeAction({
  648. before: beforeSpy,
  649. after: afterSpy
  650. })
  651. }
  652. ]
  653. })
  654. store.dispatch(TEST, 2)
  655. expect(beforeSpy).toHaveBeenCalledWith(
  656. { type: TEST, payload: 2 },
  657. store.state
  658. )
  659. expect(afterSpy).not.toHaveBeenCalled()
  660. nextTick(() => {
  661. expect(afterSpy).toHaveBeenCalledWith(
  662. { type: TEST, payload: 2 },
  663. store.state
  664. )
  665. done()
  666. })
  667. })
  668. })
  669. it('action error subscribers', (done) => {
  670. const beforeSpy = jest.fn()
  671. const afterSpy = jest.fn()
  672. const errorSpy = jest.fn()
  673. const error = new Error()
  674. const store = new Vuex.Store({
  675. actions: {
  676. [TEST]: () => Promise.reject(error)
  677. },
  678. plugins: [
  679. store => {
  680. store.subscribeAction({
  681. before: beforeSpy,
  682. after: afterSpy,
  683. error: errorSpy
  684. })
  685. }
  686. ]
  687. })
  688. store.dispatch(TEST, 2).catch(() => {
  689. expect(beforeSpy).toHaveBeenCalledWith(
  690. { type: TEST, payload: 2 },
  691. store.state
  692. )
  693. expect(afterSpy).not.toHaveBeenCalled()
  694. nextTick(() => {
  695. expect(afterSpy).not.toHaveBeenCalledWith(
  696. { type: TEST, payload: 2 },
  697. store.state
  698. )
  699. expect(errorSpy).toHaveBeenCalledWith(
  700. { type: TEST, payload: 2 },
  701. store.state,
  702. error
  703. )
  704. done()
  705. })
  706. })
  707. })
  708. it('asserts a mutation should be a function', () => {
  709. expect(() => {
  710. new Vuex.Store({
  711. mutations: {
  712. test: null
  713. }
  714. })
  715. }).toThrowError(
  716. /mutations should be function but "mutations\.test" is null/
  717. )
  718. expect(() => {
  719. new Vuex.Store({
  720. modules: {
  721. foo: {
  722. modules: {
  723. bar: {
  724. mutations: {
  725. test: 123
  726. }
  727. }
  728. }
  729. }
  730. }
  731. })
  732. }).toThrowError(
  733. /mutations should be function but "mutations\.test" in module "foo\.bar" is 123/
  734. )
  735. })
  736. it('asserts an action should be a function', () => {
  737. expect(() => {
  738. new Vuex.Store({
  739. actions: {
  740. test: 'test'
  741. }
  742. })
  743. }).toThrowError(
  744. /actions should be function or object with "handler" function but "actions\.test" is "test"/
  745. )
  746. expect(() => {
  747. new Vuex.Store({
  748. modules: {
  749. foo: {
  750. modules: {
  751. bar: {
  752. actions: {
  753. test: 'error'
  754. }
  755. }
  756. }
  757. }
  758. }
  759. })
  760. }).toThrowError(
  761. /actions should be function or object with "handler" function but "actions\.test" in module "foo\.bar" is "error"/
  762. )
  763. })
  764. it('asserts a getter should be a function', () => {
  765. expect(() => {
  766. new Vuex.Store({
  767. getters: {
  768. test: undefined
  769. }
  770. })
  771. }).toThrowError(
  772. /getters should be function but "getters\.test" is undefined/
  773. )
  774. expect(() => {
  775. new Vuex.Store({
  776. modules: {
  777. foo: {
  778. modules: {
  779. bar: {
  780. getters: {
  781. test: true
  782. }
  783. }
  784. }
  785. }
  786. }
  787. })
  788. }).toThrowError(
  789. /getters should be function but "getters\.test" in module "foo\.bar" is true/
  790. )
  791. })
  792. })