diff --git a/webapp/Dockerfile b/webapp/Dockerfile index dec3cda0e..d14c791a8 100644 --- a/webapp/Dockerfile +++ b/webapp/Dockerfile @@ -46,6 +46,7 @@ ONBUILD RUN cp -r ./constants /build ONBUILD RUN cp -r ./static /build ONBUILD RUN cp -r ./locales /build ONBUILD RUN cp -r ./package.json ./yarn.lock /build +ONBUILD RUN cp -r ./scripts /build ONBUILD RUN cd /build && yarn install --production=true --frozen-lockfile --non-interactive FROM base AS test_build @@ -62,6 +63,7 @@ RUN cp -r ./constants /build RUN cp -r ./static /build RUN cp -r ./locales /build RUN cp -r ./package.json ./yarn.lock /build +RUN cp -r ./scripts /build RUN cd /build && yarn install --frozen-lockfile --non-interactive FROM test_build AS test diff --git a/webapp/components/CommentCard/CommentCard.spec.js b/webapp/components/CommentCard/CommentCard.spec.js index 78d87e497..f4fa307fb 100644 --- a/webapp/components/CommentCard/CommentCard.spec.js +++ b/webapp/components/CommentCard/CommentCard.spec.js @@ -62,6 +62,9 @@ describe('CommentCard.vue', () => { Wrapper = () => { const store = new Vuex.Store({ getters, + actions: { + 'pinnedPosts/fetch': jest.fn(), + }, }) return mount(CommentCard, { store, diff --git a/webapp/components/CommentList/CommentList.spec.js b/webapp/components/CommentList/CommentList.spec.js index f4195aa41..d8be2375c 100644 --- a/webapp/components/CommentList/CommentList.spec.js +++ b/webapp/components/CommentList/CommentList.spec.js @@ -50,9 +50,12 @@ describe('CommentList.vue', () => { return { id: 'some-user' } }, }, + actions: { + 'pinnedPosts/fetch': jest.fn(), + }, }) mocks = { - $t: jest.fn(), + $t: (key) => key, $filters: { truncate: (a) => a, removeHtml: (a) => a, diff --git a/webapp/components/Empty/CallToAction/CtaUnblockAuthor.spec.js b/webapp/components/Empty/CallToAction/CtaUnblockAuthor.spec.js index 66e46b4b5..5f4e31ddf 100755 --- a/webapp/components/Empty/CallToAction/CtaUnblockAuthor.spec.js +++ b/webapp/components/Empty/CallToAction/CtaUnblockAuthor.spec.js @@ -4,6 +4,10 @@ import Component from './CtaUnblockAuthor.vue' const localVue = global.localVue +const stubs = { + 'nuxt-link': true, +} + describe('CtaUnblockAuthor.vue', () => { let propsData, wrapper, mocks @@ -21,7 +25,7 @@ describe('CtaUnblockAuthor.vue', () => { }) const Wrapper = () => { - return shallowMount(Component, { propsData, localVue, mocks }) + return shallowMount(Component, { propsData, localVue, mocks, stubs }) } describe('shallowMount', () => { diff --git a/webapp/components/Empty/CallToAction/__snapshots__/CtaUnblockAuthor.spec.js.snap b/webapp/components/Empty/CallToAction/__snapshots__/CtaUnblockAuthor.spec.js.snap index d3b8ad797..0264ad49e 100755 --- a/webapp/components/Empty/CallToAction/__snapshots__/CtaUnblockAuthor.spec.js.snap +++ b/webapp/components/Empty/CallToAction/__snapshots__/CtaUnblockAuthor.spec.js.snap @@ -9,10 +9,10 @@ exports[`CtaUnblockAuthor.vue shallowMount renders 1`] = ` contribution.comment.commenting-disabled.blocked-author.call-to-action - + contribution.comment.commenting-disabled.blocked-author.button-label - + `; diff --git a/webapp/components/LocationInfo/LocationInfo.spec.js b/webapp/components/LocationInfo/LocationInfo.spec.js index 7771da8ce..66d3a89e6 100644 --- a/webapp/components/LocationInfo/LocationInfo.spec.js +++ b/webapp/components/LocationInfo/LocationInfo.spec.js @@ -4,7 +4,7 @@ import LocationInfo from './LocationInfo.vue' const localVue = global.localVue describe('LocationInfo', () => { - const Wrapper = ({ withDistance }) => { + const Wrapper = ({ withDistance, size = 'base', isOwner = false }) => { return render(LocationInfo, { localVue, propsData: { @@ -12,6 +12,8 @@ describe('LocationInfo', () => { name: 'Paris', distanceToMe: withDistance ? 100 : null, }, + size, + isOwner, }, mocks: { $t: jest.fn((t) => t), @@ -33,12 +35,12 @@ describe('LocationInfo', () => { describe('size', () => { it('renders in base size', () => { - const wrapper = Wrapper({ size: 'base' }) + const wrapper = Wrapper({ withDistance: false, size: 'base' }) expect(wrapper.container).toMatchSnapshot() }) it('renders in small size', () => { - const wrapper = Wrapper({ size: 'small' }) + const wrapper = Wrapper({ withDistance: false, size: 'small' }) expect(wrapper.container).toMatchSnapshot() }) }) diff --git a/webapp/components/LocationInfo/__snapshots__/LocationInfo.spec.js.snap b/webapp/components/LocationInfo/__snapshots__/LocationInfo.spec.js.snap index e8a1e2a44..ef5030b3b 100644 --- a/webapp/components/LocationInfo/__snapshots__/LocationInfo.spec.js.snap +++ b/webapp/components/LocationInfo/__snapshots__/LocationInfo.spec.js.snap @@ -78,7 +78,7 @@ exports[`LocationInfo size renders in base size 1`] = ` exports[`LocationInfo size renders in small size 1`] = `
{ - let mocks, getters, propsData, wrapper + let mocks, getters, actions, propsData, wrapper const Wrapper = () => { const store = new Vuex.Store({ getters, + actions, }) return mount(SearchResults, { mocks, localVue, propsData, store, stubs }) } @@ -34,6 +35,9 @@ describe('SearchResults', () => { 'auth/isModerator': () => false, 'categories/categoriesActive': () => false, } + actions = { + 'categories/init': jest.fn(), + } propsData = { pageSize: 12, search: '', diff --git a/webapp/nuxt.config.js b/webapp/nuxt.config.js index 403c6fb5b..edade8f62 100644 --- a/webapp/nuxt.config.js +++ b/webapp/nuxt.config.js @@ -266,9 +266,6 @@ export default { ** You can extend webpack config here */ extend(config, ctx) { - // Fix composition api reference for v-mapbox - config.resolve.alias['@vue/composition-api'] = '@nuxtjs/composition-api' - // Add the compilerOptions ctx.loaders.vue.compilerOptions = { // Add your compilerOptions here diff --git a/webapp/package.json b/webapp/package.json index c3f5fb5b5..79dc9abad 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -18,7 +18,8 @@ "precommit": "yarn lint", "test": "cross-env NODE_ENV=test jest --coverage --forceExit --detectOpenHandles", "test:unit:update": "yarn test -- --updateSnapshot", - "test:unit:debug": "node --inspect-brk ./node_modules/jest/bin/jest.js --no-cache --runInBand" + "test:unit:debug": "node --inspect-brk ./node_modules/jest/bin/jest.js --no-cache --runInBand", + "postinstall": "node scripts/fix-vue2-jest.js && node scripts/fix-v-mapbox.js" }, "dependencies": { "@mapbox/mapbox-gl-geocoder": "^5.0.2", @@ -29,6 +30,7 @@ "@nuxtjs/pwa": "^3.0.0-beta.20", "@nuxtjs/sentry": "^4.0.0", "@nuxtjs/style-resources": "~1.1.0", + "@vue/composition-api": "^1.7.2", "accounting": "~0.4.1", "apollo-cache-inmemory": "~1.6.6", "apollo-client": "~2.6.10", diff --git a/webapp/pages/password-reset.spec.js b/webapp/pages/password-reset.spec.js index 328f04606..a753a3f4b 100644 --- a/webapp/pages/password-reset.spec.js +++ b/webapp/pages/password-reset.spec.js @@ -7,6 +7,7 @@ const localVue = global.localVue const stubs = { 'client-only': true, 'nuxt-child': true, + 'nuxt-link': true, } describe('password-reset.vue', () => { diff --git a/webapp/pages/post/_id/_slug/index.spec.js b/webapp/pages/post/_id/_slug/index.spec.js index f37108afd..da186bf72 100644 --- a/webapp/pages/post/_id/_slug/index.spec.js +++ b/webapp/pages/post/_id/_slug/index.spec.js @@ -46,6 +46,7 @@ describe('PostSlug', () => { }, actions: { 'categories/init': jest.fn(), + 'pinnedPosts/fetch': jest.fn(), }, }) const propsData = {} diff --git a/webapp/scripts/fix-v-mapbox.js b/webapp/scripts/fix-v-mapbox.js new file mode 100644 index 000000000..b2093d224 --- /dev/null +++ b/webapp/scripts/fix-v-mapbox.js @@ -0,0 +1,63 @@ +#!/usr/bin/env node +/** + * This script patches v-mapbox to fix the templateRefs issue with Vue 2.6 + @vue/composition-api. + * + * The problem: v-mapbox uses `ref(context.refs)` in setup(), but context.refs is empty + * until the component is mounted in Vue 2.6 with @vue/composition-api. + * + * The fix: Replace the setup function to use a reactive getter that accesses $refs at runtime. + */ +const fs = require('fs') +const path = require('path') + +const vmapboxFile = path.join( + __dirname, + '..', + 'node_modules', + 'v-mapbox', + 'dist', + 'v-mapbox.esm.js', +) + +if (fs.existsSync(vmapboxFile)) { + let content = fs.readFileSync(vmapboxFile, 'utf8') + + // Check if already patched + if (content.includes('// PATCHED for Vue 2.6')) { + // eslint-disable-next-line no-console + console.log('v-mapbox already patched') + process.exit(0) + } + + // Find and replace the problematic setup function + // Original: setup(_, context) { const templateRefs = ref(context.refs); return { templateRefs }; } + const originalSetup = + /setup\(_, context\)\s*\{\s*const templateRefs\s*=\s*ref\(context\.refs\);\s*return\s*\{\s*templateRefs\s*\};\s*\}/ + + const patchedSetup = `setup(_, context) { + // PATCHED for Vue 2.6 + @vue/composition-api compatibility + // Use a computed-like approach that accesses $refs at runtime + const templateRefs = ref({}); + return { templateRefs }; + }` + + if (originalSetup.test(content)) { + content = content.replace(originalSetup, patchedSetup) + + // Also patch the $_loadMap method to use this.$refs directly + content = content.replace( + /container:\s*this\.templateRefs\.container/g, + 'container: this.$refs.container', + ) + + fs.writeFileSync(vmapboxFile, content) + // eslint-disable-next-line no-console + console.log('Patched v-mapbox for Vue 2.6 compatibility') + } else { + // eslint-disable-next-line no-console + console.log('v-mapbox setup pattern not found - may already be compatible or structure changed') + } +} else { + // eslint-disable-next-line no-console + console.log('v-mapbox not installed, skipping patch') +} diff --git a/webapp/scripts/fix-vue2-jest.js b/webapp/scripts/fix-vue2-jest.js new file mode 100644 index 000000000..ccba89083 --- /dev/null +++ b/webapp/scripts/fix-vue2-jest.js @@ -0,0 +1,30 @@ +#!/usr/bin/env node +/** + * This script prevents vue2-jest from using Vue 3's compiler-sfc + * when Vue 3 is installed in parent node_modules (e.g., from vuepress). + * + * It creates a mock module at node_modules/vue/compiler-sfc that throws + * an error, forcing vue2-jest to fall back to @vue/component-compiler-utils. + */ +const fs = require('fs') +const path = require('path') + +const vueDir = path.join(__dirname, '..', 'node_modules', 'vue') +const compilerSfcDir = path.join(vueDir, 'compiler-sfc') +const indexFile = path.join(compilerSfcDir, 'index.js') + +// Only create if vue exists but compiler-sfc doesn't +if (fs.existsSync(vueDir) && !fs.existsSync(path.join(vueDir, 'compiler-sfc.js'))) { + if (!fs.existsSync(compilerSfcDir)) { + fs.mkdirSync(compilerSfcDir, { recursive: true }) + } + + const content = `// Auto-generated by scripts/fix-vue2-jest.js +// Prevents vue2-jest from using Vue 3's compiler-sfc from parent node_modules +throw new Error('vue/compiler-sfc is not available in Vue 2.6') +` + + fs.writeFileSync(indexFile, content) + // eslint-disable-next-line no-console + console.log('Created vue/compiler-sfc mock for vue2-jest compatibility') +} diff --git a/webapp/test/testSetup.js b/webapp/test/testSetup.js index a477254c8..dc1aa9829 100644 --- a/webapp/test/testSetup.js +++ b/webapp/test/testSetup.js @@ -1,3 +1,4 @@ +import Vue from 'vue' import { createLocalVue } from '@vue/test-utils' import Vuex from 'vuex' import VTooltip from 'v-tooltip' @@ -10,6 +11,20 @@ import VueObserveVisibility from '~/plugins/vue-observe-visibility' require('intersection-observer') +// Fail tests on Vue warnings +Vue.config.warnHandler = (msg, vm, trace) => { + throw new Error(`[Vue warn]: ${msg}${trace}`) +} + +// Fail tests on console.error (catches Vuex errors like "unknown action type") +// eslint-disable-next-line no-console +const originalConsoleError = console.error +// eslint-disable-next-line no-console +console.error = (...args) => { + originalConsoleError.apply(console, args) + throw new Error(`console.error was called: ${args.join(' ')}`) +} + global.localVue = createLocalVue() global.localVue.use(Vuex) diff --git a/webapp/yarn.lock b/webapp/yarn.lock index 1776bc197..dfe592405 100644 --- a/webapp/yarn.lock +++ b/webapp/yarn.lock @@ -5571,6 +5571,11 @@ source-map "~0.6.1" vue-template-es2015-compiler "^1.9.0" +"@vue/composition-api@^1.7.2": + version "1.7.2" + resolved "https://registry.yarnpkg.com/@vue/composition-api/-/composition-api-1.7.2.tgz#0b656f3ec39fefc2cf40aaa8c12426bcfeae1b44" + integrity sha512-M8jm9J/laYrYT02665HkZ5l2fWTK4dcVg3BsDHm/pfz+MjDYwX+9FUaZyGwEyXEDonQYRCo0H7aLgdklcIELjw== + "@vue/test-utils@1.3.4": version "1.3.4" resolved "https://registry.yarnpkg.com/@vue/test-utils/-/test-utils-1.3.4.tgz#83a68179178cb3da4b2b7c5c59ac660dbdff8ef5"