diff --git a/packages/portal/nuxt.config.js b/packages/portal/nuxt.config.js index ff4e360205..0f38f4b7d4 100644 --- a/packages/portal/nuxt.config.js +++ b/packages/portal/nuxt.config.js @@ -465,6 +465,13 @@ export default { loader: 'file-loader' }); + // Handle .mjs files, e.g. for @vueuse/core + config.module.rules.push({ + test: /\.mjs$/, + include: /node_modules/, + type: 'javascript/auto' + }); + // Extend webpack config only for client bundle if (isClient) { // Build source maps to aid debugging in production builds diff --git a/packages/portal/src/components/download/DownloadButton.vue b/packages/portal/src/components/download/DownloadButton.vue index ea4508495a..e5a2801886 100644 --- a/packages/portal/src/components/download/DownloadButton.vue +++ b/packages/portal/src/components/download/DownloadButton.vue @@ -22,7 +22,6 @@ diff --git a/packages/portal/src/components/share/ShareSocialButtons.vue b/packages/portal/src/components/share/ShareSocialButtons.vue index b0d59b7815..1c6be1871d 100644 --- a/packages/portal/src/components/share/ShareSocialButtons.vue +++ b/packages/portal/src/components/share/ShareSocialButtons.vue @@ -23,13 +23,11 @@ diff --git a/packages/portal/src/pages/item/_.vue b/packages/portal/src/pages/item/_.vue index 6508c15c75..ccb7c3c22e 100644 --- a/packages/portal/src/pages/item/_.vue +++ b/packages/portal/src/pages/item/_.vue @@ -142,7 +142,6 @@ import WebResource from '@/plugins/europeana/edm/WebResource.js'; import stringify from '@/utils/text/stringify.js'; import logEventMixin from '@/mixins/logEvent'; - import canonicalUrlMixin from '@/mixins/canonicalUrl'; import pageMetaMixin from '@/mixins/pageMeta'; import redirectToMixin from '@/mixins/redirectTo'; @@ -164,12 +163,13 @@ }, mixins: [ - canonicalUrlMixin, pageMetaMixin, redirectToMixin, logEventMixin ], + inject: ['canonicalUrl'], + provide() { return { // provide the deBias terms and definitions instead of using the composable @@ -282,7 +282,7 @@ year: langMapValueForLocale(this.metadata.year, this.metadataLanguage).values[0], provider: langMapValueForLocale(this.metadata.edmDataProvider, this.metadataLanguage).values[0], country: langMapValueForLocale(this.metadata.edmCountry, this.metadataLanguage).values[0], - url: this.shareUrl + url: this.canonicalUrl.withQuery }; }, titlesInCurrentLanguage() { @@ -317,9 +317,6 @@ linkForContributingAnnotation() { return this.annotationsByMotivation('linkForContributing')[0]?.body; }, - shareUrl() { - return this.canonicalUrl({ fullPath: true, locale: false }); - }, relatedEntityUris() { return this.europeanaEntityUris.filter((entityUri) => entityUri !== this.dataProviderEntityUri).slice(0, 5); }, diff --git a/packages/portal/src/pages/stories/_.vue b/packages/portal/src/pages/stories/_.vue index bca405eeb1..08fa09666e 100644 --- a/packages/portal/src/pages/stories/_.vue +++ b/packages/portal/src/pages/stories/_.vue @@ -45,7 +45,6 @@ import StoryPost from '@/components/story/StoryPost'; import logEventMixin from '@/mixins/logEvent'; import pageMetaMixin from '@/mixins/pageMeta'; - import canonicalUrlMixin from '@/mixins/canonicalUrl'; export default { name: 'StoriesPage', @@ -58,11 +57,14 @@ }, mixins: [ - canonicalUrlMixin, pageMetaMixin, logEventMixin ], + inject: [ + 'canonicalUrl' + ], + data() { return { post: {} @@ -108,7 +110,7 @@ }, mounted() { - this.logEvent('view', this.canonicalUrl({ fullPath: true, locale: false })); + this.logEvent('view', this.canonicalUrl.withOnlyQuery); } }; diff --git a/packages/portal/tests/unit/components/download/DownloadButton.spec.js b/packages/portal/tests/unit/components/download/DownloadButton.spec.js index 0d121cb438..3588eeb2d3 100644 --- a/packages/portal/tests/unit/components/download/DownloadButton.spec.js +++ b/packages/portal/tests/unit/components/download/DownloadButton.spec.js @@ -7,6 +7,8 @@ import sinon from 'sinon'; const localVue = createLocalVue(); localVue.use(BootstrapVue); +const canonicalUrl = 'https://www.europeana.eu/item/123/abc'; + const factory = ({ propsData = {}, data = {}, mocks = {} } = {}) => { const wrapper = shallowMount(DownloadButton, { localVue, @@ -15,22 +17,15 @@ const factory = ({ propsData = {}, data = {}, mocks = {} } = {}) => { mocks: { $apis: { mediaProxy: { baseURL: 'https://proxy.europeana.eu' } }, $apm: { captureError: sinon.spy() }, - $config: { - app: { - baseUrl: 'https://www.europeana.eu' - } - }, $features: {}, - $i18n: { - locale: 'de' - }, - $route: { - fullPath: '/de/item/123/abc?query=tree', - path: '/de/item/123/abc' - }, $matomo: { trackEvent: sinon.spy(), trackLink: sinon.spy() }, $t: (key) => key, ...mocks + }, + provide: { + canonicalUrl: { + withNeitherLocaleNorQuery: canonicalUrl + } } }); wrapper.vm.$refs.downloadButton.$el = { click: sinon.spy() }; @@ -342,8 +337,6 @@ describe('components/download/DownloadButton', () => { }); describe('trackDownload', () => { - const canonicalUrl = 'https://www.europeana.eu/item/123/abc'; - describe('the first download', () => { it('tracks both the file and custom download event', async() => { const wrapper = factory({ propsData }); diff --git a/packages/portal/tests/unit/components/provide/ProvideCanonicalUrl.spec.js b/packages/portal/tests/unit/components/provide/ProvideCanonicalUrl.spec.js new file mode 100644 index 0000000000..2b0da3352c --- /dev/null +++ b/packages/portal/tests/unit/components/provide/ProvideCanonicalUrl.spec.js @@ -0,0 +1,155 @@ +import { createLocalVue } from '@vue/test-utils'; +import { mountNuxt } from '../../utils.js'; +import ProvideCanonicalUrl from '@/components/provide/ProvideCanonicalUrl.vue'; + +const localVue = createLocalVue(); + +const fixtures = { + config: { app: { baseUrl: 'https://www.example.org' } }, + i18n: { locale: 'en' }, + routes: { + home: { + path: '/en', + fullPath: '/en?query=art' + }, + item: { + path: '/en/item/123/abc', + fullPath: '/en/item/123/abc?query=art' + } + } +}; +const factory = ({ mocks = {}, slots = {} } = {}) => mountNuxt(ProvideCanonicalUrl, { + localVue, + mocks: { + $config: fixtures.config, + $i18n: fixtures.i18n, + $route: fixtures.routes.home, + ...mocks + }, + slots: { + ...slots + } +}); + +describe('components/provide/ProvideCanonicalUrl', () => { + describe('canonicalUrl', () => { + describe('.withBothLocaleAndQuery', () => { + it('includes both locale and query for home page', () => { + const mocks = { $route: fixtures.routes.home }; + const wrapper = factory({ mocks }); + + const withBothLocaleAndQuery = wrapper.vm.canonicalUrl.withBothLocaleAndQuery; + + expect(withBothLocaleAndQuery).toBe('https://www.example.org/en?query=art'); + }); + + it('includes both locale and query for non-home page', () => { + const mocks = { $route: fixtures.routes.item }; + const wrapper = factory({ mocks }); + + const withBothLocaleAndQuery = wrapper.vm.canonicalUrl.withBothLocaleAndQuery; + + expect(withBothLocaleAndQuery).toBe('https://www.example.org/en/item/123/abc?query=art'); + }); + }); + + describe('.withOnlyQuery', () => { + it('includes only query for home page', () => { + const mocks = { $route: fixtures.routes.home }; + const wrapper = factory({ mocks }); + + const withOnlyQuery = wrapper.vm.canonicalUrl.withOnlyQuery; + + expect(withOnlyQuery).toBe('https://www.example.org/?query=art'); + }); + + it('includes only query for non-home page', () => { + const mocks = { $route: fixtures.routes.item }; + const wrapper = factory({ mocks }); + + const withOnlyQuery = wrapper.vm.canonicalUrl.withOnlyQuery; + + expect(withOnlyQuery).toBe('https://www.example.org/item/123/abc?query=art'); + }); + }); + + describe('.withOnlyLocale', () => { + it('includes only locale for home page', () => { + const mocks = { $route: fixtures.routes.home }; + const wrapper = factory({ mocks }); + + const withOnlyLocale = wrapper.vm.canonicalUrl.withOnlyLocale; + + expect(withOnlyLocale).toBe('https://www.example.org/en'); + }); + + it('includes only locale for non-home page', () => { + const mocks = { $route: fixtures.routes.item }; + const wrapper = factory({ mocks }); + + const withOnlyLocale = wrapper.vm.canonicalUrl.withOnlyLocale; + + expect(withOnlyLocale).toBe('https://www.example.org/en/item/123/abc'); + }); + }); + + describe('.withNeitherLocaleNorQuery', () => { + it('includes neither locale nor query for home page', () => { + const mocks = { $route: fixtures.routes.home }; + const wrapper = factory({ mocks }); + + const withNeitherLocaleNorQuery = wrapper.vm.canonicalUrl.withNeitherLocaleNorQuery; + + expect(withNeitherLocaleNorQuery).toBe('https://www.example.org/'); + }); + + it('includes neither locale nor query for non-home page', () => { + const mocks = { $route: fixtures.routes.item }; + const wrapper = factory({ mocks }); + + const withNeitherLocaleNorQuery = wrapper.vm.canonicalUrl.withNeitherLocaleNorQuery; + + expect(withNeitherLocaleNorQuery).toBe('https://www.example.org/item/123/abc'); + }); + }); + }); + + describe('provide', () => { + it('provides canonicalUrl to slotted child components', () => { + const ChildComponent = { + inject: ['canonicalUrl'], + template: 'Link' + }; + const slots = { default: ChildComponent }; + const wrapper = factory({ slots }); + + const href = wrapper.find('a').attributes('href'); + + expect(href).toBe('https://www.example.org/en?query=art'); + }); + }); + + describe('head', () => { + describe('link', () => { + it('includes a link for [hreflang="x-default"][rel="alternate"] with query, no locale', () => { + const mocks = { $route: fixtures.routes.home }; + const wrapper = factory({ mocks }); + + const link = wrapper.vm.head().link.find((l) => l.hreflang === 'x-default' && l.rel === 'alternate'); + + expect(link.href).toBe('https://www.example.org/?query=art'); + }); + }); + + describe('meta', () => { + it('includes meta for [property="og-url"] with both locale and query', () => { + const mocks = { $route: fixtures.routes.home }; + const wrapper = factory({ mocks }); + + const meta = wrapper.vm.head().meta.find((m) => m.property === 'og:url'); + + expect(meta.content).toBe('https://www.example.org/en?query=art'); + }); + }); + }); +}); diff --git a/packages/portal/tests/unit/components/share/ShareSocialButtons.spec.js b/packages/portal/tests/unit/components/share/ShareSocialButtons.spec.js index 01237875c7..643c50b80e 100644 --- a/packages/portal/tests/unit/components/share/ShareSocialButtons.spec.js +++ b/packages/portal/tests/unit/components/share/ShareSocialButtons.spec.js @@ -15,22 +15,13 @@ const factory = () => shallowMount(ShareSocialButtons, { mediaUrl: '/img/portrait.jpg' }, mocks: { - $config: { - app: { - baseUrl: 'https://www.example.org' - } - }, - $i18n: { - locale: 'fr' - }, - $route: { - fullPath: '/page', - path: '/page' - }, $matomo: { trackEvent: sinon.spy() }, $t: () => {} + }, + provide: { + canonicalUrl: { withOnlyQuery: 'https://www.example.org/page' } } }); diff --git a/packages/portal/tests/unit/layouts/default.spec.js b/packages/portal/tests/unit/layouts/default.spec.js index 37f4a49bf1..f4042b636f 100644 --- a/packages/portal/tests/unit/layouts/default.spec.js +++ b/packages/portal/tests/unit/layouts/default.spec.js @@ -44,9 +44,6 @@ const factory = (options = {}) => shallowMountNuxt(layout, { $announcer: { setComplementRoute: () => {} }, - $exp: { - $variantIndexes: [0] - }, $route: { query: {}, fullPath: '/fr', @@ -130,14 +127,6 @@ describe('layouts/default.vue', () => { expect(wrapper.vm.head().meta.filter(anyTag => nuxtI18nHead.meta.some(i18nTag => i18nTag.hid === anyTag.hid))).toEqual(nuxtI18nHead.meta); }); - it('includes og:url with canonical URL', () => { - const wrapper = factory(); - - const headMeta = wrapper.vm.head().meta; - - expect(headMeta.find((tag) => tag.property === 'og:url').content).toBe('https://www.example.org/fr'); - }); - it('includes description "Europeana"', () => { const wrapper = factory(); diff --git a/packages/portal/tests/unit/layouts/ds4ch.spec.js b/packages/portal/tests/unit/layouts/ds4ch.spec.js index 2285e42925..e5a0f8190c 100644 --- a/packages/portal/tests/unit/layouts/ds4ch.spec.js +++ b/packages/portal/tests/unit/layouts/ds4ch.spec.js @@ -10,8 +10,10 @@ localVue.use(BootstrapVue); const factory = (options = {}) => shallowMountNuxt(layout, { localVue, mocks: { + $config: { app: { baseUrl: 'https://www.example.org', siteName: 'Europeana' } }, + $i18n: { locale: 'en' }, $t: key => key, - $route: {}, + $route: { path: '/ds4ch', fullPath: '/ds4ch' }, $features: {}, ...options.mocks }, diff --git a/packages/portal/tests/unit/layouts/landing.spec.js b/packages/portal/tests/unit/layouts/landing.spec.js index b039b13d87..e317013b91 100644 --- a/packages/portal/tests/unit/layouts/landing.spec.js +++ b/packages/portal/tests/unit/layouts/landing.spec.js @@ -11,8 +11,9 @@ const factory = (options = {}) => shallowMountNuxt(layout, { localVue, mocks: { $config: { app: { baseUrl: 'https://www.example.org', siteName: 'Europeana' } }, + $i18n: { locale: 'en' }, $t: key => key, - $route: { fullPath: '/landing' }, + $route: { path: '/landing', fullPath: '/landing' }, $features: {}, ...options.mocks }, @@ -46,15 +47,5 @@ describe('layouts/landing.vue', () => { expect(iconLink.type).toEqual('image/x-icon'); }); }); - - describe('meta', () => { - it('includes og:url with canonical URL', () => { - const wrapper = factory(); - - const headMeta = wrapper.vm.head().meta; - - expect(headMeta.find((tag) => tag.property === 'og:url').content).toBe('https://www.example.org/landing'); - }); - }); }); }); diff --git a/packages/portal/tests/unit/mixins/canonicalUrl.spec.js b/packages/portal/tests/unit/mixins/canonicalUrl.spec.js deleted file mode 100644 index 84286b3234..0000000000 --- a/packages/portal/tests/unit/mixins/canonicalUrl.spec.js +++ /dev/null @@ -1,124 +0,0 @@ -import { createLocalVue } from '@vue/test-utils'; -import { shallowMountNuxt } from '../utils'; - -import mixin from '@/mixins/canonicalUrl'; - -const component = { - template: '
', - mixins: [mixin] -}; - -const localVue = createLocalVue(); - -const factory = ({ mocks = {} } = {}) => shallowMountNuxt(component, { - localVue, - mocks: { - $config: { - app: { - baseUrl: 'https://www.example.org' - } - }, - $i18n: { - locale: 'en' - }, - $route: {}, - ...mocks - } -}); - -const fixtures = { - routes: { - home: { - path: '/en', - fullPath: '/en?query=art' - }, - item: { - path: '/en/item/123/abc', - fullPath: '/en/item/123/abc?query=art' - } - } -}; - -describe('mixins/canonicalUrl', () => { - describe('methods', () => { - describe('canonicalUrl', () => { - describe('with fullPath: true', () => { - const fullPath = true; - - describe('and locale: true', () => { - const locale = true; - it('concatenates base URL and route full path, keeping locale', () => { - const wrapper = factory({ mocks: { $route: fixtures.routes.home } }); - - const canonicalUrl = wrapper.vm.canonicalUrl({ fullPath, locale }); - - expect(canonicalUrl).toBe('https://www.example.org/en?query=art'); - }); - }); - - describe('and locale: false', () => { - const locale = false; - - describe('on the homepage', () => { - it('concatenates base URL and route full path, removing locale', () => { - const wrapper = factory({ mocks: { $route: fixtures.routes.home } }); - - const canonicalUrl = wrapper.vm.canonicalUrl({ fullPath, locale }); - - expect(canonicalUrl).toBe('https://www.example.org/?query=art'); - }); - }); - - describe('on an item page', () => { - it('concatenates base URL and route full path, removing locale', () => { - const wrapper = factory({ mocks: { $route: fixtures.routes.item } }); - - const canonicalUrl = wrapper.vm.canonicalUrl({ fullPath, locale }); - - expect(canonicalUrl).toBe('https://www.example.org/item/123/abc?query=art'); - }); - }); - }); - }); - - describe('with fullPath: false', () => { - const fullPath = false; - - describe('and locale: true', () => { - const locale = true; - it('concatenates base URL and route path, keeping locale', () => { - const wrapper = factory({ mocks: { $route: fixtures.routes.home } }); - - const canonicalUrl = wrapper.vm.canonicalUrl({ fullPath, locale }); - - expect(canonicalUrl).toBe('https://www.example.org/en'); - }); - }); - - describe('and locale: false', () => { - const locale = false; - - describe('on the homepage', () => { - it('concatenates base URL and route path, removing locale', () => { - const wrapper = factory({ mocks: { $route: fixtures.routes.home } }); - - const canonicalUrl = wrapper.vm.canonicalUrl({ fullPath, locale }); - - expect(canonicalUrl).toBe('https://www.example.org/'); - }); - }); - - describe('on an item page', () => { - it('concatenates base URL and route path, removing locale', () => { - const wrapper = factory({ mocks: { $route: fixtures.routes.item } }); - - const canonicalUrl = wrapper.vm.canonicalUrl({ fullPath, locale }); - - expect(canonicalUrl).toBe('https://www.example.org/item/123/abc'); - }); - }); - }); - }); - }); - }); -}); diff --git a/packages/portal/tests/unit/pages/exhibitions/exhibition/index.spec.js b/packages/portal/tests/unit/pages/exhibitions/exhibition/index.spec.js index cf6bacf2df..fb66472fbc 100644 --- a/packages/portal/tests/unit/pages/exhibitions/exhibition/index.spec.js +++ b/packages/portal/tests/unit/pages/exhibitions/exhibition/index.spec.js @@ -28,6 +28,9 @@ const factory = (options = defaultOptions) => shallowMountNuxt(page, { mixins: [ exhibitionChapters ], + provide: { + canonicalUrl: {} + }, data() { return { identifier: 'exhibition', @@ -70,18 +73,8 @@ const factory = (options = defaultOptions) => shallowMountNuxt(page, { $store: { commit: sinon.spy() }, - $route: { - fullPath: 'https://www.europeana.eu/en/exhibitions/exhibition', - path: '/en/exhibitions/exhibition' - }, - $i18n: { - locale: 'en' - }, - $config: { - app: { - baseUrl: 'https://www.europeana.eu' - } - } + $route: {}, + $i18n: {} } }); diff --git a/packages/portal/tests/unit/pages/item/_.spec.js b/packages/portal/tests/unit/pages/item/_.spec.js index c69e4ba992..5f58e4c4e2 100644 --- a/packages/portal/tests/unit/pages/item/_.spec.js +++ b/packages/portal/tests/unit/pages/item/_.spec.js @@ -180,9 +180,7 @@ const factory = ({ data = {}, mocks = {} } = {}) => shallowMountNuxt(page, { ...fixtures.auth.notLoggedIn, ...fixtures.route.standard, $config: { - app: { - baseUrl: 'https://www.example.org' - }, + app: {}, matomo: {} }, $features: { translatedItems: true }, @@ -216,6 +214,9 @@ const factory = ({ data = {}, mocks = {} } = {}) => shallowMountNuxt(page, { $error: sinon.spy(), $session: { isActive: false }, ...mocks + }, + provide: { + canonicalUrl: {} } }); diff --git a/packages/portal/tests/unit/pages/stories/_.spec.js b/packages/portal/tests/unit/pages/stories/_.spec.js index 9deb94f182..4b34bf2fc2 100644 --- a/packages/portal/tests/unit/pages/stories/_.spec.js +++ b/packages/portal/tests/unit/pages/stories/_.spec.js @@ -32,11 +32,6 @@ const factory = ({ data = {} } = {}) => shallowMountNuxt(page, { }; }, mocks: { - $config: { - app: { - baseUrl: 'https://www.europeana.eu' - } - }, $contentful: { query: contentfulQuery }, @@ -56,6 +51,9 @@ const factory = ({ data = {} } = {}) => shallowMountNuxt(page, { path: '/en/stories/once-upon-a-time', query: {} } + }, + provide: { + canonicalUrl: {} } });