From 3c4646bff6e8f527150d3d47cb76e11f032241cd Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Thu, 18 Nov 2021 13:29:42 +0100 Subject: [PATCH 1/4] Delete ModuleNode caches on class change --- .../graphql/gql/local/mutations/deleteModuleNodes.gql | 3 +++ client/src/graphql/resolvers.js | 6 ++++++ client/src/mixins/update-selected-class.js | 10 ++++++++++ 3 files changed, 19 insertions(+) create mode 100644 client/src/graphql/gql/local/mutations/deleteModuleNodes.gql diff --git a/client/src/graphql/gql/local/mutations/deleteModuleNodes.gql b/client/src/graphql/gql/local/mutations/deleteModuleNodes.gql new file mode 100644 index 00000000..bc0c1939 --- /dev/null +++ b/client/src/graphql/gql/local/mutations/deleteModuleNodes.gql @@ -0,0 +1,3 @@ +mutation { + deleteModuleNodes @client +} diff --git a/client/src/graphql/resolvers.js b/client/src/graphql/resolvers.js index 2d0c2620..8dafbd5a 100644 --- a/client/src/graphql/resolvers.js +++ b/client/src/graphql/resolvers.js @@ -34,5 +34,11 @@ export const resolvers = { cache.writeQuery({query: SIDEBAR, data}); return data.sidebar; }, + deleteModuleNodes: (_, _query, {cache}) => { + Object.keys(cache.data.data) + .filter(prop => prop.indexOf('ModuleNode:') === 0) + .map(moduleName => cache.data.delete(moduleName)); // v3 uses .evict{id, fieldname} + return {success: true} + }, }, }; diff --git a/client/src/mixins/update-selected-class.js b/client/src/mixins/update-selected-class.js index 62b2bf7d..fc4a6b98 100644 --- a/client/src/mixins/update-selected-class.js +++ b/client/src/mixins/update-selected-class.js @@ -1,10 +1,12 @@ import ME_QUERY from '@/graphql/gql/queries/meQuery.gql'; import UPDATE_USER_SETTING from '@/graphql/gql/mutations/updateUserSetting.gql'; import MY_SCHOOL_CLASS_QUERY from '@/graphql/gql/queries/mySchoolClass.gql'; +import DELETE_MODULE_NODES from '@/graphql/gql/local/mutations/deleteModuleNodes.gql'; export default { methods: { updateSelectedClass(selectedClass) { + const innerApollo = this.$apollo; return this.$apollo.mutate({ mutation: UPDATE_USER_SETTING, variables: { @@ -18,6 +20,14 @@ export default { meData.me.selectedClass = selectedClass; store.writeQuery({query: ME_QUERY, data: meData}); + + innerApollo.mutate({ + mutation: DELETE_MODULE_NODES, + variables: { + test: '', + } + }) + }, refetchQueries: [{ query: MY_SCHOOL_CLASS_QUERY From ca82a080d11ed4f109e059e2b96d24e26a9deba6 Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Thu, 18 Nov 2021 13:39:55 +0100 Subject: [PATCH 2/4] Refetch current module after class change --- client/src/mixins/update-selected-class.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/client/src/mixins/update-selected-class.js b/client/src/mixins/update-selected-class.js index fc4a6b98..d137e725 100644 --- a/client/src/mixins/update-selected-class.js +++ b/client/src/mixins/update-selected-class.js @@ -2,6 +2,7 @@ import ME_QUERY from '@/graphql/gql/queries/meQuery.gql'; import UPDATE_USER_SETTING from '@/graphql/gql/mutations/updateUserSetting.gql'; import MY_SCHOOL_CLASS_QUERY from '@/graphql/gql/queries/mySchoolClass.gql'; import DELETE_MODULE_NODES from '@/graphql/gql/local/mutations/deleteModuleNodes.gql'; +import MODULE_DETAILS_QUERY from "@/graphql/gql/queries/modules/moduleDetailsQuery.gql"; export default { methods: { @@ -31,6 +32,11 @@ export default { }, refetchQueries: [{ query: MY_SCHOOL_CLASS_QUERY + }, { + query: MODULE_DETAILS_QUERY, + variables: { + slug: this.$route.params.slug + } }] }).catch((error) => { console.warn('failed to update selected class', error); From 00ae2a4cf21dc4c494f149cc97715a3496e657fc Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Tue, 23 Nov 2021 07:17:51 +0100 Subject: [PATCH 3/4] Remove unused variable, make linter happy --- client/src/graphql/resolvers.js | 4 ++-- client/src/mixins/update-selected-class.js | 20 ++++++++------------ 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/client/src/graphql/resolvers.js b/client/src/graphql/resolvers.js index 8dafbd5a..29375292 100644 --- a/client/src/graphql/resolvers.js +++ b/client/src/graphql/resolvers.js @@ -37,8 +37,8 @@ export const resolvers = { deleteModuleNodes: (_, _query, {cache}) => { Object.keys(cache.data.data) .filter(prop => prop.indexOf('ModuleNode:') === 0) - .map(moduleName => cache.data.delete(moduleName)); // v3 uses .evict{id, fieldname} - return {success: true} + .map(moduleName => cache.data.delete(moduleName)); // v3 uses .evict{id, fieldname} + return {success: true}; }, }, }; diff --git a/client/src/mixins/update-selected-class.js b/client/src/mixins/update-selected-class.js index d137e725..cc9637de 100644 --- a/client/src/mixins/update-selected-class.js +++ b/client/src/mixins/update-selected-class.js @@ -2,7 +2,7 @@ import ME_QUERY from '@/graphql/gql/queries/meQuery.gql'; import UPDATE_USER_SETTING from '@/graphql/gql/mutations/updateUserSetting.gql'; import MY_SCHOOL_CLASS_QUERY from '@/graphql/gql/queries/mySchoolClass.gql'; import DELETE_MODULE_NODES from '@/graphql/gql/local/mutations/deleteModuleNodes.gql'; -import MODULE_DETAILS_QUERY from "@/graphql/gql/queries/modules/moduleDetailsQuery.gql"; +import MODULE_DETAILS_QUERY from '@/graphql/gql/queries/modules/moduleDetailsQuery.gql'; export default { methods: { @@ -23,21 +23,17 @@ export default { store.writeQuery({query: ME_QUERY, data: meData}); innerApollo.mutate({ - mutation: DELETE_MODULE_NODES, - variables: { - test: '', - } - }) - + mutation: DELETE_MODULE_NODES + }); }, refetchQueries: [{ query: MY_SCHOOL_CLASS_QUERY }, { - query: MODULE_DETAILS_QUERY, - variables: { - slug: this.$route.params.slug - } - }] + query: MODULE_DETAILS_QUERY, + variables: { + slug: this.$route.params.slug + } + }] }).catch((error) => { console.warn('failed to update selected class', error); }); From c4183b3469f5413e34035d630c5c072d419e032a Mon Sep 17 00:00:00 2001 From: Christian Cueni Date: Tue, 23 Nov 2021 15:06:05 +0100 Subject: [PATCH 4/4] Add test --- client/package-lock.json | 6 + client/package.json | 1 + .../tests/unit/class-selection-widget.spec.js | 195 ++++++++++++++++++ 3 files changed, 202 insertions(+) create mode 100644 client/tests/unit/class-selection-widget.spec.js diff --git a/client/package-lock.json b/client/package-lock.json index cf68088b..fef36c7d 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -13280,6 +13280,12 @@ } } }, + "mock-apollo-client": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/mock-apollo-client/-/mock-apollo-client-0.7.0.tgz", + "integrity": "sha512-r0ICU01m007W0MwMej0lzlg1REtepDZ15Fyj8Hz9tiW/1TPb0PyHryGykrg9YhfbB8/+ZF2ovz+88yMF75TDoA==", + "dev": true + }, "moment": { "version": "2.29.1", "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", diff --git a/client/package.json b/client/package.json index 6aa5c78f..213b7f2a 100644 --- a/client/package.json +++ b/client/package.json @@ -128,6 +128,7 @@ "jest-transform-graphql": "^2.1.0", "jest-transform-stub": "^2.0.0", "jest-watch-typeahead": "^0.3.1", + "mock-apollo-client": "^0.7.0", "ts-loader": "^8.3.0", "typescript": "^4.4.3", "vue-jest": "^3.0.4" diff --git a/client/tests/unit/class-selection-widget.spec.js b/client/tests/unit/class-selection-widget.spec.js new file mode 100644 index 00000000..1865f962 --- /dev/null +++ b/client/tests/unit/class-selection-widget.spec.js @@ -0,0 +1,195 @@ +import {createLocalVue, mount} from '@vue/test-utils' +import { createMockClient } from 'mock-apollo-client' + +import SIDEBAR from '@/graphql/gql/local/sidebar.gql'; +import ME_QUERY from '@/graphql/gql/queries/meQuery.gql'; +import UPDATE_USER_SETTING from '@/graphql/gql/mutations/updateUserSetting.gql'; +import MY_SCHOOL_CLASS_QUERY from '@/graphql/gql/queries/mySchoolClass.gql'; +import DELETE_MODULE_NODES from '@/graphql/gql/local/mutations/deleteModuleNodes.gql'; +import MODULE_DETAILS_QUERY from '@/graphql/gql/queries/modules/moduleDetailsQuery.gql'; + +import VueApollo from 'vue-apollo' +import ClassSelectionWidget from '@/components/school-class/ClassSelectionWidget'; + + + +// https://dev.to/n_tepluhina/testing-vue-apollo-2020-edition-2l2p + +const localVue = createLocalVue(); +localVue.use(VueApollo); + +const updateSettingsResponse = { + data: { + updateSetting: { + success: true, + errors:null, + __typename: 'UpdateSettingPayload' + } + } +}; + +const querySidebarResponse = { + data: {} +}; + +const deleteModulesResponse = { + data: {} +}; + +const meQueryResponse = { + data: { + me: { + __typename: "PrivateUserNode", + id: "UHJpdmF0ZVVzZXJOb2RlOjI=", + pk: 2, + username: "ross.geller", + email: "ross.geller@skillbox.example", + firstName: "Ross", + lastName: "Geller", + avatarUrl: "https://ucarecdn.com/fe10f9cc-a509-4170-9396-258abc418247/", + expiryDate: "2021-11-29", + readOnly: false, + lastModule: null, + lastTopic: { + id: "VG9waWNOb2RlOjQ5", + slug: "berufliche-grundbildung", + __typename: "TopicNode" + }, + selectedClass: { + id: "U2Nob29sQ2xhc3NOb2RlOjM=", + readOnly: false, + __typename: "SchoolClassNode" + }, + recentModules: { + edges: [], + __typename: "ModuleNodeConnection" + }, + schoolClasses: { + edges: [ + { + node: { + id: "U2Nob29sQ2xhc3NOb2RlOjE=", + name: "Friends", + __typename: "SchoolClassNode" + }, + __typename: "SchoolClassNodeEdge" + }, + { + node: { + id: "U2Nob29sQ2xhc3NOb2RlOjM=", + name: "ghfgfh", + __typename: "SchoolClassNode" + }, + __typename: "SchoolClassNodeEdge" + } + ], + __typename: "SchoolClassNodeConnection" + }, + team: null, + isTeacher: true, + permissions: [ + "users.can_manage_school_class_content" + ], + onboardingVisited: true + } + } +} + +const mySchoolClassResponse = { + data: {} +}; + +const moduleDetailResponse = { + data: {} +}; + +const slug = '/some/123'; +const schoolClasses = [ + { + id: 'abcd123', + name: 'Hello' + },{ + id: 'xyz098', + name: 'Kitty' + } +]; + +describe('ClassSelectionWidget.vue', () => { + + let mockClient; + let apolloProvider; + let wrapper; + + const requestHandlers = { + updateSettingsHandler: jest.fn().mockResolvedValueOnce(updateSettingsResponse), + querySidebarHandler: jest.fn().mockResolvedValueOnce(querySidebarResponse), + meQueryHandler: jest.fn().mockResolvedValueOnce(meQueryResponse), + deleteModulesHandler: jest.fn().mockResolvedValueOnce(deleteModulesResponse), + mySchoolClassHandler: jest.fn().mockResolvedValueOnce(mySchoolClassResponse), + moduleDetailHandler: jest.fn().mockResolvedValueOnce(moduleDetailResponse), + } + + + const createComponent = () => { + mockClient = createMockClient(); + mockClient.cache.writeQuery = jest.fn(); + + mockClient.setRequestHandler(UPDATE_USER_SETTING, requestHandlers.updateSettingsHandler); + mockClient.setRequestHandler(SIDEBAR, requestHandlers.querySidebarHandler); + mockClient.setRequestHandler(ME_QUERY, requestHandlers.meQueryHandler); + mockClient.setRequestHandler(DELETE_MODULE_NODES, requestHandlers.deleteModulesHandler); + mockClient.setRequestHandler(MY_SCHOOL_CLASS_QUERY, requestHandlers.mySchoolClassHandler); + mockClient.setRequestHandler(MODULE_DETAILS_QUERY, requestHandlers.moduleDetailHandler); + + apolloProvider = new VueApollo({ + defaultClient: mockClient, + }); + + wrapper = mount(ClassSelectionWidget, { + localVue, + apolloProvider, + mocks: { + $route: { + params: { + slug + } + } + } + }); + + wrapper.vm.me.schoolClass = schoolClasses; + + } + + + it('should delete the modules cache and query the current module on a class change', async () => { + + createComponent(); + + wrapper.vm.me.selectedClass = {id: 'abcd123'} + wrapper.vm.updateSelectedClassAndHidePopover(schoolClasses[1]); + + expect(requestHandlers.updateSettingsHandler).toHaveBeenCalledWith({ + input: { + id: schoolClasses[1].id + } + }); + + await wrapper.vm.$nextTick(); + + // modules have been removed from cache + expect(requestHandlers.deleteModulesHandler).toHaveBeenCalled(); + expect(requestHandlers.mySchoolClassHandler).toHaveBeenCalled(); + // current module is being refetched + expect(requestHandlers.moduleDetailHandler).toHaveBeenCalledWith({slug}); + + }); + + afterEach(() => { + wrapper.destroy() + mockClient = null + apolloProvider = null + }) + + +})