Repository: effector/effector
Pull request #1008 (2023-11-03 11:58:24)

Tests for EffectorScopePlugin
### Changelog
- Add test for `EffectorScopePlugin` from `effector-vue`
- Fix scopefull store reading in `useUnit` from `effector-vue/composition`


Patch:
diff --git a/src/vue/lib/create-watch.ts b/src/vue/lib/create-watch.ts
deleted file mode 100644
index a70b7993..00000000
--- a/src/vue/lib/create-watch.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-/* eslint-disable @typescript-eslint/no-explicit-any */
-import {Store, clearNode, step, createNode, Scope, Node} from "effector"
-
-export function createWatch<T>(
-  store: Store<T>,
-  fn: (value: T) => any,
-  scope?: Scope,
-) {
-  let seq = [step.run({fn: (value) => fn(value)})]
-  if (scope) {
-    let node = createNode({node: seq})
-    let id = (store as any).graphite.id
-    let scopeLinks: {[_: string]: Node[]} = (scope as any).additionalLinks
-    // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
-    let links = scopeLinks[id] || []
-    scopeLinks[id] = links
-    links.push(node)
-    return () => {
-      let idx = links.indexOf(node)
-      if (idx !== -1) links.splice(idx, 1)
-      clearNode(node)
-    }
-  } else {
-    let node = createNode({
-      node: seq,
-      parent: [store],
-      family: {owners: store},
-    })
-    return () => {
-      clearNode(node)
-    }
-  }
-}
diff --git a/src/vue/lib/get-scope.ts b/src/vue/lib/get-scope.ts
index 1b21a899..b6e6f992 100644
--- a/src/vue/lib/get-scope.ts
+++ b/src/vue/lib/get-scope.ts
@@ -1,10 +1,11 @@
-import {Scope} from "effector"
-import {getCurrentInstance, inject} from "vue-next"
+import {Scope} from 'effector'
+import {getCurrentInstance, inject} from 'vue-next'
 
 export function getScope() {
   let scope: Scope | undefined
   let ctx = getCurrentInstance()
-  let scopeName: string | undefined = ctx?.appContext.config.globalProperties.scopeName
+  let scopeName: string | undefined =
+    ctx?.appContext.config.globalProperties.scopeName
 
   if (scopeName) {
     scope = inject(scopeName)
diff --git a/src/vue/useStore.ts b/src/vue/useStore.ts
index df1309f5..2374665c 100644
--- a/src/vue/useStore.ts
+++ b/src/vue/useStore.ts
@@ -1,25 +1,24 @@
-import {is, Store} from "effector"
-import {onUnmounted, readonly, shallowRef} from "vue-next"
+import {is, createWatch, Store} from 'effector'
+import {onUnmounted, readonly, shallowRef} from 'vue-next'
 
-import {createWatch} from "./lib/create-watch"
-import {stateReader} from "./lib/state-reader"
-import {getScope} from "./lib/get-scope"
-import {throwError} from "./lib/throw"
+import {stateReader} from './lib/state-reader'
+import {getScope} from './lib/get-scope'
+import {throwError} from './lib/throw'
 
 export function useStore<T>(store: Store<T>) {
-  if (!is.store(store)) throwError("expect useStore argument to be a store")
+  if (!is.store(store)) throwError('expect useStore argument to be a store')
   let {scope} = getScope()
 
   let state = stateReader(store, scope)
   let _ = shallowRef(state)
 
-  let stop = createWatch(
-    store,
-    (value) => {
+  let stop = createWatch({
+    unit: store,
+    fn: value => {
       _.value = shallowRef(value).value
     },
-    scope
-  )
+    scope,
+  })
 
   onUnmounted(() => {
     stop()
diff --git a/src/vue/useUnit.ts b/src/vue/useUnit.ts
index ff3c906c..fac2179d 100644
--- a/src/vue/useUnit.ts
+++ b/src/vue/useUnit.ts
@@ -43,7 +43,7 @@ export function useUnit<Shape extends {[key: string]: Unit<any>}>(
   const states: Record<string, any> = {}
   for (const key of storeKeys) {
     // @ts-expect-error TS can't infer that normShape[key] is a Store
-    const state = stateReader(normShape[key])
+    const state = stateReader(normShape[key], scope)
     const ref = shallowRef(state)
     const stop = createWatch({
       unit: normShape[key],
diff --git a/src/vue/useVModel.ts b/src/vue/useVModel.ts
index 918ecf0f..767deeac 100644
--- a/src/vue/useVModel.ts
+++ b/src/vue/useVModel.ts
@@ -1,28 +1,29 @@
-import {Store, is, createStore} from "effector"
-import {onUnmounted, reactive, Ref, ref, watch} from "vue-next"
-
-import {unwrapProxy} from "./lib/unwrapProxy"
-import {deepCopy} from "./lib/deepCopy"
-import {stateReader} from "./lib/state-reader"
-import {createWatch} from "./lib/create-watch"
-import {getScope} from "./lib/get-scope"
-import { UseVModel } from "effector-vue/composition"
-
-function createVModel<T>(store: Store<T>, key?: string, shape?: Record<string, unknown>) {
-  if (!is.store(store)) throw Error("expect useVModel argument to be a store")
+import {Store, is, createWatch} from 'effector'
+import {onUnmounted, reactive, Ref, ref, watch} from 'vue-next'
+
+import {unwrapProxy} from './lib/unwrapProxy'
+import {deepCopy} from './lib/deepCopy'
+import {stateReader} from './lib/state-reader'
+import {getScope} from './lib/get-scope'
+import {UseVModel} from 'effector-vue/composition'
+
+function createVModel<T>(
+  store: Store<T>,
+  key?: string,
+  shape?: Record<string, unknown>,
+) {
+  if (!is.store(store)) throw Error('expect useVModel argument to be a store')
 
   let {scope} = getScope()
 
-  let _ = ref(
-    deepCopy(stateReader(store, scope))
-  )
+  let _ = ref(deepCopy(stateReader(store, scope)))
 
   let isSelfUpdate = false
   let fromEvent = false
 
-  let stop = createWatch(
-    store,
-    (payload) => {
+  let stop = createWatch({
+    unit: store,
+    fn: payload => {
       if (isSelfUpdate) {
         return
       }
@@ -30,8 +31,8 @@ function createVModel<T>(store: Store<T>, key?: string, shape?: Record<string, u
       fromEvent = true
       _.value = ref(deepCopy(payload)).value
     },
-    scope
-  )
+    scope,
+  })
 
   onUnmounted(() => {
     stop()
@@ -46,7 +47,7 @@ function createVModel<T>(store: Store<T>, key?: string, shape?: Record<string, u
 
   watch(
     watchFn,
-    (value) => {
+    value => {
       isSelfUpdate = true
 
       if (!fromEvent) {
@@ -64,20 +65,24 @@ function createVModel<T>(store: Store<T>, key?: string, shape?: Record<string, u
   return _ as Ref<T>
 }
 
-function isStore<T,>(arg: Store<T> | Record<string, unknown>): arg is Store<T> {
-  return is.store(arg)
-}
-
 // @ts-expect-error
-export const useVModel: UseVModel = <T, K extends string = keyof Store<unknown>>(vm: Store<T> | Record<K, Store<T>>) => {
-  if (isStore(vm)) {
+export const useVModel: UseVModel = <
+  T,
+  K extends string = keyof Store<unknown>,
+>(
+  vm: Store<T> | Record<K, Store<T>>,
+) => {
+  if (is.store(vm)) {
     return createVModel(vm)
   }
 
   const _ = reactive({})
 
   const shape = Object.fromEntries(
-    Object.entries<Store<T>>(vm).map(([key, value]) => [key, createVModel(value, key, _)])
+    Object.entries<Store<T>>(vm).map(([key, value]) => [
+      key,
+      createVModel(value, key, _),
+    ]),
   )
 
   for (const key in shape) {


Test patch:
diff --git a/src/vue/__tests__/EffectorScopePlugin.test.ts b/src/vue/__tests__/EffectorScopePlugin.test.ts
new file mode 100644
index 00000000..642ff18d
--- /dev/null
+++ b/src/vue/__tests__/EffectorScopePlugin.test.ts
@@ -0,0 +1,55 @@
+import {shallowMount} from 'vue-test-utils-next'
+import {EffectorScopePlugin} from 'effector-vue'
+import {useUnit} from 'effector-vue/composition'
+import {createEvent, createStore, fork, allSettled} from 'effector'
+
+jest.mock('vue', () => require('vue-next'))
+
+describe('EffectorScopePlugin', () => {
+  test('does not mix scopes', async () => {
+    const $value = createStore('No scope')
+
+    const setValue = createEvent<string>()
+
+    $value.on(setValue, (_, value) => value)
+
+    const App = {
+      template: `
+        <div>
+          <p data-test="value">{{value}}</p>
+        </div>
+      `,
+      setup() {
+        const value = useUnit($value)
+
+        return {
+          value,
+        }
+      },
+    }
+
+    const scopeOne = fork({values: [[$value, 'scope one']]})
+    const scopeTwo = fork({values: [[$value, 'scope two']]})
+
+    const wrapperOne = shallowMount(App, {
+      global: {plugins: [EffectorScopePlugin({scope: scopeOne})]},
+    })
+
+    const wrapperTwo = shallowMount(App, {
+      global: {plugins: [EffectorScopePlugin({scope: scopeTwo})]},
+    })
+
+    expect(wrapperOne.find('[data-test="value"]').text()).toBe('scope one')
+    expect(wrapperTwo.find('[data-test="value"]').text()).toBe('scope two')
+
+    await allSettled(setValue, {scope: scopeOne, params: 'scope one updated'})
+    await allSettled(setValue, {scope: scopeTwo, params: 'scope two updated'})
+
+    expect(wrapperOne.find('[data-test="value"]').text()).toBe(
+      'scope one updated',
+    )
+    expect(wrapperTwo.find('[data-test="value"]').text()).toBe(
+      'scope two updated',
+    )
+  })
+})
diff --git a/src/vue/__tests__/useUnit.test.ts b/src/vue/__tests__/useUnit.test.ts
index e52c0f6d..c1d7fd6c 100644
--- a/src/vue/__tests__/useUnit.test.ts
+++ b/src/vue/__tests__/useUnit.test.ts
@@ -1,6 +1,6 @@
 /* eslint-disable react-hooks/rules-of-hooks */
 import {shallowMount} from 'vue-test-utils-next'
-import {VueSSRPlugin} from 'effector-vue/ssr'
+import {EffectorScopePlugin} from 'effector-vue'
 import {useUnit} from 'effector-vue/composition'
 import {createEvent, createStore, createWatch, fork} from 'effector'
 import {watchEffect} from 'vue-next'
@@ -340,7 +340,7 @@ describe('useUnit', () => {
             return {greet}
           },
         },
-        {global: {plugins: [VueSSRPlugin({scope: correnctScope})]}},
+        {global: {plugins: [EffectorScopePlugin({scope: correnctScope})]}},
       )
 
       const correctListener = jest.fn()
@@ -369,8 +369,6 @@ describe('useUnit', () => {
       const correnctScope = fork()
       const incorrectScope = fork()
 
-      const someEvent = createEvent()
-
       const userAdded = createEvent()
       const $users = createStore([{name: 'John', surname: 'Doe'}])
 
@@ -391,7 +389,7 @@ describe('useUnit', () => {
             return {users, add}
           },
         },
-        {global: {plugins: [VueSSRPlugin({scope: correnctScope})]}},
+        {global: {plugins: [EffectorScopePlugin({scope: correnctScope})]}},
       )
 
       expect(wrapper.findAll('[data-test="item"]')).toHaveLength(1)