fix(webapp): allow running frontend tests locally (#9125)

This commit is contained in:
Ulf Gebhardt 2026-01-24 21:09:36 +01:00 committed by GitHub
parent ba481547f1
commit af497deb77
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 145 additions and 13 deletions

View File

@ -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

View File

@ -62,6 +62,9 @@ describe('CommentCard.vue', () => {
Wrapper = () => {
const store = new Vuex.Store({
getters,
actions: {
'pinnedPosts/fetch': jest.fn(),
},
})
return mount(CommentCard, {
store,

View File

@ -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,

View File

@ -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', () => {

View File

@ -9,10 +9,10 @@ exports[`CtaUnblockAuthor.vue shallowMount renders 1`] = `
<ds-text-stub tag="p">
contribution.comment.commenting-disabled.blocked-author.call-to-action
</ds-text-stub>
<nuxt-link to="[object Object]">
<nuxt-link-stub to="[object Object]">
<base-button-stub filled="true" icon="arrow-right" size="regular" type="button">
contribution.comment.commenting-disabled.blocked-author.button-label
</base-button-stub>
</nuxt-link>
</nuxt-link-stub>
</ds-space-stub>
`;

View File

@ -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()
})
})

View File

@ -78,7 +78,7 @@ exports[`LocationInfo size renders in base size 1`] = `
exports[`LocationInfo size renders in small size 1`] = `
<div>
<div
class="location-info size-base"
class="location-info size-small"
>
<div
class="location"

View File

@ -15,10 +15,11 @@ const stubs = {
}
describe('SearchResults', () => {
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: '',

View File

@ -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

View File

@ -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",

View File

@ -7,6 +7,7 @@ const localVue = global.localVue
const stubs = {
'client-only': true,
'nuxt-child': true,
'nuxt-link': true,
}
describe('password-reset.vue', () => {

View File

@ -46,6 +46,7 @@ describe('PostSlug', () => {
},
actions: {
'categories/init': jest.fn(),
'pinnedPosts/fetch': jest.fn(),
},
})
const propsData = {}

View File

@ -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')
}

View File

@ -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')
}

View File

@ -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)

View File

@ -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"