/*! * Matomo - free/libre analytics platform * * @link https://matomo.org * @license https://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later */ import { flushPromises, mount } from '@vue/test-utils'; // The grid mounts the real SparklineCard -> NoComparison -> MetricValue chain, so the mock // provides everything that chain pulls from CoreHome: the Tooltips directive (MetricValue), // MatomoUrl (SparklineCard derives graph-params from the url) and NumberFormatter // (NoComparison formats raw numeric metric values). CoreHome has no jest module mapping. jest.mock('CoreHome', () => ({ Tooltips: {}, Sparkline: { template: '' }, // SparklineCard calls MatomoUrl.parse to derive data-graph-params, but no test here // asserts on it and the fixture urls carry no columns/rows/idGoal, so an empty object // is enough to satisfy the `parsed[key]` lookups without crashing. MatomoUrl: { parse: () => ({}), }, NumberFormatter: { formatNumber: (value: number) => String(value), }, }), { virtual: true }); // eslint-disable-next-line @typescript-eslint/no-var-requires const SparklinesGrid = require('./SparklinesGrid.vue').default; describe('CoreVisualizations/SparklinesGrid', () => { let initializeSparklinesSpy: jest.Mock; beforeEach(() => { // sparkline.js (which defines window.initializeSparklines) is loaded on every real // Matomo page but not in the jest bootstrap, so stub it here. initializeSparklinesSpy = jest.fn(); window.initializeSparklines = initializeSparklinesSpy; }); function entry(description: string, order = 1) { return { url: '?module=API&action=get', metrics: { '': [{ value: '1', description }] }, order, title: null, group: '0', seriesIndices: null, graphParams: null, }; } function placeholder(order: number) { // Mirrors Config::addPlaceholder(): empty url + no metrics, used only for legacy layout. return { url: '', metrics: {}, order, title: null, group: `placeholder${order}`, seriesIndices: null, graphParams: null, }; } function createWrapper(props: Record = {}) { // eslint-disable-next-line @typescript-eslint/no-explicit-any return mount(SparklinesGrid as any, { props: { sparklines: { 0: [entry('Visits')], 1: [entry('Actions'), entry('Bounce rate')] }, ...props, }, }); } it('flattens grouped sparklines into one card per entry', () => { const wrapper = createWrapper(); expect(wrapper.findAllComponents({ name: 'SparklineCard' }).length).toBe(3); }); it('drops layout placeholders so they do not render as empty cards', () => { // The flagship Visits Overview adds placeholders (order 10/31/50) purely for the legacy // 2-column layout; they have an empty url and no metrics and must not reach the grid. const wrapper = createWrapper({ sparklines: { 0: [entry('Visits', 1)], 1: [placeholder(10)], 2: [entry('Actions', 20)], }, }); const titles = wrapper.findAll('.metricValue__title').map((node) => node.text()); expect(wrapper.findAllComponents({ name: 'SparklineCard' }).length).toBe(2); expect(titles).toEqual(['Visits', 'Actions']); }); it('forwards allMetricsDocumentation to each card, surfacing it as the title tooltip', () => { const wrapper = createWrapper({ sparklines: { 0: [{ ...entry('Visits'), metrics: { '': [{ value: '1', description: 'Visits', column: 'nb_visits' }] } }] }, allMetricsDocumentation: { nb_visits: 'The number of visits.' }, }); const card = wrapper.findComponent({ name: 'SparklineCard' }); expect(card.props('allMetricsDocumentation')).toEqual({ nb_visits: 'The number of visits.' }); expect(wrapper.find('.metricValue__title').attributes('title')).toBe('The number of visits.'); }); it('uses the responsive grid columns (s6 m6 l4 xl3) on reporting pages', () => { const wrapper = createWrapper(); const col = wrapper.find('.row.sparklinesGrid > div'); expect(col.classes()).toEqual(expect.arrayContaining(['col', 's6', 'm6', 'l4', 'xl3'])); }); it('orders cards by backend `order`, not by numeric group-key iteration order', () => { // The group keys are the metric index, and getSortedSparklines() may insert them out // of numeric order (e.g. a later metric with a lower `order`). Object.values() iterates // numeric keys ascending, so without the explicit sort the cards would render as // Third, First, Second here instead of in `order`. const wrapper = createWrapper({ sparklines: { 1: [entry('First', 1)], 0: [entry('Third', 30)], 2: [entry('Second', 20)], }, }); const titles = wrapper.findAll('.metricValue__title').map((node) => node.text()); expect(titles).toEqual(['First', 'Second', 'Third']); }); it('collapses to two columns in widget mode', () => { const wrapper = createWrapper({ isWidget: true }); const col = wrapper.find('.row.sparklinesGrid > div'); expect(col.classes()).toContain('s6'); expect(col.classes()).not.toContain('xl3'); }); it('re-runs the sparkline click-to-evolution wiring after mount', async () => { createWrapper(); await flushPromises(); expect(initializeSparklinesSpy).toHaveBeenCalledTimes(1); }); });