File size: 5,439 Bytes
76a7a50 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 | /*!
* 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: '<img class="sparkline-stub" />' },
// 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<string, unknown> = {}) {
// 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);
});
});
|