@raketa-cloud/admin 1.8.17 → 1.8.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/application.css +288 -11
- package/package.json +8 -6
- package/src/handlers/resources/ReportsAPIHandler.js +27 -0
- package/src/handlers/resources/ResourcesHandler.js +16 -0
- package/src/helpers/content_tasks_helper.js +1 -1
- package/src/helpers/reports/cwv.js +217 -0
- package/src/helpers/reports/googleAuth.js +149 -0
- package/src/helpers/reports/quick-insights.js +294 -0
- package/src/helpers/reports/reportsUtils.js +181 -0
- package/src/index.js +8 -0
- package/src/localCache.js +112 -0
- package/src/report/analytics.js +107 -0
- package/src/report/reports.js +390 -0
- package/src/routeAssets.js +74 -0
- package/src/routeResourceModule.js +24 -0
- package/src/routeStandard.js +7 -1
- package/src/styles/application.css +1 -1
- package/src/tests/handlers/RedirectsHandler.test/notes.test.js +56 -0
- package/src/tests/handlers/_shared/SettingsHandler.test/global_variables.test.js +54 -0
- package/src/tests/handlers/categories/update_defaults.test.js +35 -0
- package/src/tests/handlers/content_tasks/ContentTasksHandler.test/editors.test.js +29 -0
- package/src/tests/handlers/resource_revisions/PublishesHandler.test/schedule.test.js +137 -0
- package/src/tests/handlers/resource_revisions/TagHandler.test/default_locale_tags.test.js +162 -0
- package/src/tests/handlers/resource_revisions/WidgetHandler.test/defaults.test.js +61 -0
- package/src/tests/handlers/resources/DeletesHandler.test/no_redirect.test.js +55 -0
- package/src/tests/handlers/resources/ReportsAPIHandler.test/show.test.js +156 -0
- package/src/tests/handlers/resources/ResourcesHandler.test/show.test.js +8 -0
- package/src/tests/handlers/resources/ResourcesHandler.test/show_reports.test.js +94 -0
- package/src/tests/handlers/resources/TranslationsHandler.test/edge_cases.test.js +211 -0
- package/src/tests/helpers/CSVExportHelper.branches.test.js +57 -0
- package/src/tests/helpers/FormBuilder.branches.test.js +151 -0
- package/src/tests/helpers/HTMLBuilder.test.js +51 -0
- package/src/tests/helpers/content_tasks_helper.test.js +1 -1
- package/src/tests/helpers/historyUtils.branches.test.js +36 -0
- package/src/tests/helpers/reports/cwv.test.js +239 -0
- package/src/tests/helpers/reports/quick-insights.test.js +593 -0
- package/src/tests/helpers/reports/reportsUtils.test.js +425 -0
- package/src/tests/helpers/utils.test.js +37 -0
- package/src/tests/helpers/view_helpers.test.js +87 -0
- package/src/tests/index.test.js +74 -0
- package/src/tests/localCache.test.js +236 -0
- package/src/tests/report/analytics.test.js +230 -0
- package/src/tests/report/reports.test.js +831 -0
- package/src/tests/routeAssets.test.js +120 -0
- package/src/tests/routeResourceModule.test.js +99 -2
- package/src/tests/routeStandard.test.js +3 -3
- package/src/tests/support/fakeDom.js +164 -0
- package/src/views/content_tasks/index.ejs +2 -2
- package/src/views/resources/_shared/_core_web_vitals.ejs +27 -0
- package/src/views/resources/_shared/_quick_insights.ejs +64 -0
- package/src/views/resources/_shared/_scheduled_revisions.ejs +88 -0
- package/src/views/resources/_shared/_title.ejs +16 -12
- package/src/views/resources/_shared/_toolbar.ejs +11 -21
- package/src/views/resources/_shared/_traffic_chart.ejs +27 -0
- package/src/views/resources/_shared/_translations_strip.ejs +13 -25
- package/src/views/resources/_shared/_vital_meter.ejs +54 -0
- package/src/views/resources/index.ejs +1 -1
- package/src/views/resources/show.ejs +88 -98
- package/src/views/resources/translations/create_form.ejs +14 -5
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
import localCache from "../localCache";
|
|
2
|
+
|
|
3
|
+
// The module keeps one LRU on globalThis; going through the public API keeps
|
|
4
|
+
// the tests honest about that shared instance.
|
|
5
|
+
const keys = () => {
|
|
6
|
+
const collected = [];
|
|
7
|
+
localCache.forEach((_value, key) => collected.push(key));
|
|
8
|
+
return collected;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
describe("localCache", () => {
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
localCache.clear();
|
|
14
|
+
jest.spyOn(console, "error").mockImplementation(() => {});
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
jest.restoreAllMocks();
|
|
19
|
+
jest.useRealTimers();
|
|
20
|
+
localCache.clear();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test("calls the fetcher on a miss and returns its value", async () => {
|
|
24
|
+
const fetcher = jest.fn(async () => ({ id: "1" }));
|
|
25
|
+
|
|
26
|
+
expect(await localCache.fetch("resource:1", fetcher)).toEqual({ id: "1" });
|
|
27
|
+
expect(fetcher).toHaveBeenCalledTimes(1);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("serves the second read from the cache", async () => {
|
|
31
|
+
const fetcher = jest.fn(async () => ({ id: "1" }));
|
|
32
|
+
|
|
33
|
+
await localCache.fetch("resource:1", fetcher);
|
|
34
|
+
const cached = await localCache.fetch("resource:1", fetcher);
|
|
35
|
+
|
|
36
|
+
expect(cached).toEqual({ id: "1" });
|
|
37
|
+
expect(fetcher).toHaveBeenCalledTimes(1);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("caches a missing resource so the miss is not re-fetched", async () => {
|
|
41
|
+
const fetcher = jest.fn(async () => null);
|
|
42
|
+
|
|
43
|
+
expect(await localCache.fetch("resource:missing", fetcher)).toBeNull();
|
|
44
|
+
expect(await localCache.fetch("resource:missing", fetcher)).toBeNull();
|
|
45
|
+
expect(fetcher).toHaveBeenCalledTimes(1);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Only undefined means "nothing here" — 0, false and "" are answers.
|
|
49
|
+
test.each([
|
|
50
|
+
["a zero", 0],
|
|
51
|
+
["false", false],
|
|
52
|
+
["an empty string", ""],
|
|
53
|
+
["an empty object", {}],
|
|
54
|
+
])("round-trips %s through the cache", async (_label, value) => {
|
|
55
|
+
const fetcher = jest.fn(async () => value);
|
|
56
|
+
|
|
57
|
+
expect(await localCache.fetch("resource:falsy", fetcher)).toEqual(value);
|
|
58
|
+
expect(await localCache.fetch("resource:falsy", fetcher)).toEqual(value);
|
|
59
|
+
expect(fetcher).toHaveBeenCalledTimes(1);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// JSON.stringify(undefined) is undefined and sizeCalculation would throw on
|
|
63
|
+
// it, taking the whole request with it.
|
|
64
|
+
test("caches an undefined result as a miss", async () => {
|
|
65
|
+
const fetcher = jest.fn(async () => undefined);
|
|
66
|
+
|
|
67
|
+
expect(await localCache.fetch("resource:undefined", fetcher)).toBeNull();
|
|
68
|
+
expect(await localCache.fetch("resource:undefined", fetcher)).toBeNull();
|
|
69
|
+
expect(fetcher).toHaveBeenCalledTimes(1);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
test("logs and returns null when the fetcher throws on a miss", async () => {
|
|
73
|
+
const error = new Error("cloud down");
|
|
74
|
+
const fetcher = jest.fn(async () => {
|
|
75
|
+
throw error;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
expect(await localCache.fetch("resource:1", fetcher)).toBeNull();
|
|
79
|
+
expect(console.error).toHaveBeenCalledWith(error);
|
|
80
|
+
// Nothing was stored, so the next caller retries straight away.
|
|
81
|
+
expect(keys()).toEqual([]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
test("honours a per-entry ttl", async () => {
|
|
85
|
+
await localCache.fetch("resource:1", async () => ({ id: "1" }), {
|
|
86
|
+
ttl: 50_000,
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
expect(localCache.getRemainingTTL("resource:1")).toBeGreaterThan(40_000);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test("clear empties the cache", async () => {
|
|
93
|
+
await localCache.fetch("resource:1", async () => ({ id: "1" }));
|
|
94
|
+
|
|
95
|
+
localCache.clear();
|
|
96
|
+
|
|
97
|
+
expect(keys()).toEqual([]);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
test("reuses the single cache instance across module loads", async () => {
|
|
101
|
+
await localCache.fetch("resource:1", async () => ({ id: "1" }));
|
|
102
|
+
|
|
103
|
+
let reloaded;
|
|
104
|
+
jest.isolateModules(() => {
|
|
105
|
+
reloaded = require("../localCache").default;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const seen = [];
|
|
109
|
+
reloaded.forEach((_value, key) => seen.push(key));
|
|
110
|
+
|
|
111
|
+
expect(seen).toEqual(["resource:1"]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("forEach walks the cached entries", async () => {
|
|
115
|
+
await localCache.fetch("resource:1", async () => ({ id: "1" }));
|
|
116
|
+
await localCache.fetch("resource:2", async () => ({ id: "2" }));
|
|
117
|
+
|
|
118
|
+
expect(keys().sort()).toEqual(["resource:1", "resource:2"]);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
describe("stale entries", () => {
|
|
122
|
+
// lru-cache captures its own clock at import time, so fake timers do not
|
|
123
|
+
// reach it — expire for real with a throwaway ttl instead. The cache keeps
|
|
124
|
+
// stale entries around (allowStale + noDeleteOnStaleGet), so the next read
|
|
125
|
+
// is a "stale" hit rather than a miss.
|
|
126
|
+
const TTL = 20;
|
|
127
|
+
const expire = () => new Promise((resolve) => setTimeout(resolve, TTL * 2));
|
|
128
|
+
|
|
129
|
+
test("refreshes a stale entry", async () => {
|
|
130
|
+
const fetcher = jest
|
|
131
|
+
.fn()
|
|
132
|
+
.mockResolvedValueOnce({ id: "1", v: 1 })
|
|
133
|
+
.mockResolvedValueOnce({ id: "1", v: 2 });
|
|
134
|
+
|
|
135
|
+
await localCache.fetch("resource:1", fetcher, { ttl: TTL });
|
|
136
|
+
await expire();
|
|
137
|
+
|
|
138
|
+
expect(
|
|
139
|
+
await localCache.fetch("resource:1", fetcher, { ttl: TTL }),
|
|
140
|
+
).toEqual({ id: "1", v: 2 });
|
|
141
|
+
expect(fetcher).toHaveBeenCalledTimes(2);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("serves the stale value once more when the refresh fails", async () => {
|
|
145
|
+
const error = new Error("cloud down");
|
|
146
|
+
const fetcher = jest
|
|
147
|
+
.fn()
|
|
148
|
+
.mockResolvedValueOnce({ id: "1", v: 1 })
|
|
149
|
+
.mockRejectedValueOnce(error);
|
|
150
|
+
|
|
151
|
+
await localCache.fetch("resource:1", fetcher, { ttl: TTL });
|
|
152
|
+
await expire();
|
|
153
|
+
|
|
154
|
+
expect(
|
|
155
|
+
await localCache.fetch("resource:1", fetcher, { ttl: TTL }),
|
|
156
|
+
).toEqual({ id: "1", v: 1 });
|
|
157
|
+
expect(console.error).toHaveBeenCalledWith(error);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test("resets the ttl after a failed refresh so the stale value survives", async () => {
|
|
161
|
+
const fetcher = jest
|
|
162
|
+
.fn()
|
|
163
|
+
.mockResolvedValueOnce({ id: "1", v: 1 })
|
|
164
|
+
.mockRejectedValueOnce(new Error("cloud down"));
|
|
165
|
+
|
|
166
|
+
await localCache.fetch("resource:1", fetcher, { ttl: TTL });
|
|
167
|
+
await expire();
|
|
168
|
+
await localCache.fetch("resource:1", fetcher, { ttl: 60_000 });
|
|
169
|
+
|
|
170
|
+
expect(localCache.getRemainingTTL("resource:1")).toBeGreaterThan(50_000);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("keeps returning null for a stale missing resource", async () => {
|
|
174
|
+
const fetcher = jest.fn(async () => {
|
|
175
|
+
throw new Error("cloud down");
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
await localCache.fetch("resource:missing", async () => null, {
|
|
179
|
+
ttl: TTL,
|
|
180
|
+
});
|
|
181
|
+
await expire();
|
|
182
|
+
|
|
183
|
+
expect(
|
|
184
|
+
await localCache.fetch("resource:missing", fetcher, { ttl: TTL }),
|
|
185
|
+
).toBeNull();
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
// The sentinel is what "nothing here" looks like in the cache; a failed
|
|
189
|
+
// refresh resets the ttl and must put back what it found, not the null the
|
|
190
|
+
// reader was handed.
|
|
191
|
+
test("keeps a stale undefined entry as undefined after a failed refresh", async () => {
|
|
192
|
+
const stored = () => {
|
|
193
|
+
let found;
|
|
194
|
+
localCache.forEach((value) => {
|
|
195
|
+
found = value;
|
|
196
|
+
});
|
|
197
|
+
return found;
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
await localCache.fetch("resource:undefined", async () => undefined, {
|
|
201
|
+
ttl: TTL,
|
|
202
|
+
});
|
|
203
|
+
const sentinel = stored();
|
|
204
|
+
await expire();
|
|
205
|
+
|
|
206
|
+
await localCache.fetch(
|
|
207
|
+
"resource:undefined",
|
|
208
|
+
async () => {
|
|
209
|
+
throw new Error("cloud down");
|
|
210
|
+
},
|
|
211
|
+
{ ttl: TTL },
|
|
212
|
+
);
|
|
213
|
+
|
|
214
|
+
expect(stored()).toBe(sentinel);
|
|
215
|
+
expect(typeof sentinel).toBe("symbol");
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
test("stores a fresh miss found while refreshing a stale entry", async () => {
|
|
219
|
+
const fetcher = jest
|
|
220
|
+
.fn()
|
|
221
|
+
.mockResolvedValueOnce({ id: "1" })
|
|
222
|
+
.mockResolvedValueOnce(null);
|
|
223
|
+
|
|
224
|
+
await localCache.fetch("resource:1", fetcher, { ttl: TTL });
|
|
225
|
+
await expire();
|
|
226
|
+
|
|
227
|
+
expect(
|
|
228
|
+
await localCache.fetch("resource:1", fetcher, { ttl: 60_000 }),
|
|
229
|
+
).toBeNull();
|
|
230
|
+
expect(
|
|
231
|
+
await localCache.fetch("resource:1", fetcher, { ttl: 60_000 }),
|
|
232
|
+
).toBeNull();
|
|
233
|
+
expect(fetcher).toHaveBeenCalledTimes(2);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
});
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { createDom, el, installDom, uninstallDom } from "../support/fakeDom";
|
|
2
|
+
|
|
3
|
+
// The script is an IIFE that wires itself to the globals on import, so every
|
|
4
|
+
// test re-requires it with a fresh DOM.
|
|
5
|
+
function loadScript() {
|
|
6
|
+
jest.isolateModules(() => {
|
|
7
|
+
require("../../report/analytics");
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function buildChartStub() {
|
|
12
|
+
const instances = [];
|
|
13
|
+
|
|
14
|
+
class Chart {
|
|
15
|
+
constructor(ctx, config) {
|
|
16
|
+
this.ctx = ctx;
|
|
17
|
+
this.config = config;
|
|
18
|
+
this.destroy = jest.fn();
|
|
19
|
+
instances.push(this);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return { Chart, instances };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function canvas() {
|
|
27
|
+
const context = { createLinearGradient: jest.fn() };
|
|
28
|
+
const element = el("canvas", { id: "ga-traffic-chart" });
|
|
29
|
+
element.getContext = jest.fn(() => context);
|
|
30
|
+
return { element, context };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const CONFIG = {
|
|
34
|
+
series: {
|
|
35
|
+
dates: ["2026-07-04", "2026-07-05", "2026-07-06"],
|
|
36
|
+
labels: ["July 4", "July 5", "July 6"],
|
|
37
|
+
data: [10, 20, 30],
|
|
38
|
+
},
|
|
39
|
+
publishDates: ["2026-07-05"],
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
describe("report/analytics.js", () => {
|
|
43
|
+
let chartStub;
|
|
44
|
+
|
|
45
|
+
beforeEach(() => {
|
|
46
|
+
chartStub = buildChartStub();
|
|
47
|
+
globalThis.Chart = chartStub.Chart;
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
afterEach(() => {
|
|
51
|
+
uninstallDom();
|
|
52
|
+
delete globalThis.Chart;
|
|
53
|
+
jest.restoreAllMocks();
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
test("draws the inlined Insights data on load", () => {
|
|
57
|
+
const target = canvas();
|
|
58
|
+
const dom = createDom([target.element]);
|
|
59
|
+
dom.window.GA_ANALYTICS = CONFIG;
|
|
60
|
+
installDom(dom);
|
|
61
|
+
|
|
62
|
+
loadScript();
|
|
63
|
+
|
|
64
|
+
expect(chartStub.instances).toHaveLength(1);
|
|
65
|
+
|
|
66
|
+
const { config } = chartStub.instances[0];
|
|
67
|
+
expect(target.element.getContext).toHaveBeenCalledWith("2d");
|
|
68
|
+
expect(config.type).toEqual("line");
|
|
69
|
+
expect(config.data.labels).toEqual(CONFIG.series.labels);
|
|
70
|
+
expect(config.data.datasets[0].data).toEqual(CONFIG.series.data);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test("waits for DOMContentLoaded while the document is still loading", () => {
|
|
74
|
+
const dom = createDom([canvas().element], { readyState: "loading" });
|
|
75
|
+
dom.window.GA_ANALYTICS = CONFIG;
|
|
76
|
+
installDom(dom);
|
|
77
|
+
|
|
78
|
+
loadScript();
|
|
79
|
+
|
|
80
|
+
expect(chartStub.instances).toHaveLength(0);
|
|
81
|
+
|
|
82
|
+
dom.document.dispatchEvent("DOMContentLoaded");
|
|
83
|
+
|
|
84
|
+
expect(chartStub.instances).toHaveLength(1);
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("draws nothing when no data is inlined", () => {
|
|
88
|
+
installDom(createDom([canvas().element]));
|
|
89
|
+
|
|
90
|
+
loadScript();
|
|
91
|
+
|
|
92
|
+
expect(chartStub.instances).toHaveLength(0);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test("exposes gaRenderTrafficChart for reports.js", () => {
|
|
96
|
+
const dom = createDom([canvas().element]);
|
|
97
|
+
installDom(dom);
|
|
98
|
+
|
|
99
|
+
loadScript();
|
|
100
|
+
|
|
101
|
+
expect(typeof dom.window.gaRenderTrafficChart).toEqual("function");
|
|
102
|
+
|
|
103
|
+
dom.window.gaRenderTrafficChart(CONFIG);
|
|
104
|
+
|
|
105
|
+
expect(chartStub.instances).toHaveLength(1);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("replaces the previous chart instead of stacking a second one", () => {
|
|
109
|
+
const dom = createDom([canvas().element]);
|
|
110
|
+
dom.window.GA_ANALYTICS = CONFIG;
|
|
111
|
+
installDom(dom);
|
|
112
|
+
|
|
113
|
+
loadScript();
|
|
114
|
+
dom.window.gaRenderTrafficChart(CONFIG);
|
|
115
|
+
|
|
116
|
+
expect(chartStub.instances).toHaveLength(2);
|
|
117
|
+
expect(chartStub.instances[0].destroy).toHaveBeenCalled();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Chart.js refuses a canvas it still has a chart registered against, and the
|
|
121
|
+
// local handle goes stale the moment the canvas is replaced.
|
|
122
|
+
test("destroys whatever chart owns the canvas, not just its own handle", () => {
|
|
123
|
+
const target = canvas();
|
|
124
|
+
const dom = createDom([target.element]);
|
|
125
|
+
dom.window.GA_ANALYTICS = CONFIG;
|
|
126
|
+
installDom(dom);
|
|
127
|
+
|
|
128
|
+
const orphan = { destroy: jest.fn() };
|
|
129
|
+
chartStub.Chart.getChart = jest.fn(() => orphan);
|
|
130
|
+
|
|
131
|
+
loadScript();
|
|
132
|
+
dom.window.gaRenderTrafficChart(CONFIG);
|
|
133
|
+
|
|
134
|
+
expect(chartStub.Chart.getChart).toHaveBeenCalledWith(target.element);
|
|
135
|
+
expect(orphan.destroy).toHaveBeenCalled();
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test("does nothing without a canvas", () => {
|
|
139
|
+
const dom = createDom([]);
|
|
140
|
+
dom.window.GA_ANALYTICS = CONFIG;
|
|
141
|
+
installDom(dom);
|
|
142
|
+
|
|
143
|
+
loadScript();
|
|
144
|
+
|
|
145
|
+
expect(chartStub.instances).toHaveLength(0);
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
test("does nothing when Chart.js has not loaded", () => {
|
|
149
|
+
delete globalThis.Chart;
|
|
150
|
+
const dom = createDom([canvas().element]);
|
|
151
|
+
dom.window.GA_ANALYTICS = CONFIG;
|
|
152
|
+
installDom(dom);
|
|
153
|
+
|
|
154
|
+
expect(() => loadScript()).not.toThrow();
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
test("falls back to empty series when the config is missing pieces", () => {
|
|
158
|
+
const dom = createDom([canvas().element]);
|
|
159
|
+
installDom(dom);
|
|
160
|
+
|
|
161
|
+
loadScript();
|
|
162
|
+
dom.window.gaRenderTrafficChart();
|
|
163
|
+
|
|
164
|
+
const { config } = chartStub.instances[0];
|
|
165
|
+
expect(config.data.labels).toEqual([]);
|
|
166
|
+
expect(config.data.datasets[0].data).toEqual([]);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
describe("dataset callbacks", () => {
|
|
170
|
+
let dataset;
|
|
171
|
+
let options;
|
|
172
|
+
let context;
|
|
173
|
+
|
|
174
|
+
beforeEach(() => {
|
|
175
|
+
const target = canvas();
|
|
176
|
+
const dom = createDom([target.element]);
|
|
177
|
+
dom.window.GA_ANALYTICS = CONFIG;
|
|
178
|
+
installDom(dom);
|
|
179
|
+
loadScript();
|
|
180
|
+
|
|
181
|
+
context = target.context;
|
|
182
|
+
dataset = chartStub.instances[0].config.data.datasets[0];
|
|
183
|
+
options = chartStub.instances[0].config.options;
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
test("builds a vertical gradient from the chart area", () => {
|
|
187
|
+
const gradient = { addColorStop: jest.fn() };
|
|
188
|
+
context.createLinearGradient.mockReturnValue(gradient);
|
|
189
|
+
|
|
190
|
+
const result = dataset.backgroundColor({
|
|
191
|
+
chart: { ctx: context, chartArea: { top: 0, bottom: 200 } },
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
expect(context.createLinearGradient).toHaveBeenCalledWith(0, 200, 0, 0);
|
|
195
|
+
expect(gradient.addColorStop).toHaveBeenCalledTimes(2);
|
|
196
|
+
expect(result).toBe(gradient);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
test("skips the gradient before the chart area is measured", () => {
|
|
200
|
+
const result = dataset.backgroundColor({
|
|
201
|
+
chart: { ctx: context, chartArea: null },
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
expect(result).toBeNull();
|
|
205
|
+
expect(context.createLinearGradient).not.toHaveBeenCalled();
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
test("highlights points that land on a publish date", () => {
|
|
209
|
+
expect(dataset.pointRadius({ dataIndex: 1 })).toEqual(6);
|
|
210
|
+
expect(dataset.pointHoverRadius({ dataIndex: 1 })).toEqual(10);
|
|
211
|
+
expect(dataset.pointBackgroundColor({ dataIndex: 1 })).toEqual("#facc15");
|
|
212
|
+
expect(dataset.pointBorderColor({ dataIndex: 1 })).toEqual("#eab308");
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
test("leaves ordinary points unhighlighted", () => {
|
|
216
|
+
expect(dataset.pointRadius({ dataIndex: 0 })).toEqual(4);
|
|
217
|
+
expect(dataset.pointHoverRadius({ dataIndex: 0 })).toEqual(8);
|
|
218
|
+
expect(dataset.pointBackgroundColor({ dataIndex: 0 })).toEqual("#fff");
|
|
219
|
+
expect(dataset.pointBorderColor({ dataIndex: 0 })).toEqual("#6366f1");
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test("annotates tooltips with the publish state", () => {
|
|
223
|
+
const { label, afterLabel } = options.plugins.tooltip.callbacks;
|
|
224
|
+
|
|
225
|
+
expect(label({ raw: 42 })).toEqual(" Views: 42");
|
|
226
|
+
expect(afterLabel({ dataIndex: 1 })).toEqual("Revision published");
|
|
227
|
+
expect(afterLabel({ dataIndex: 0 })).toEqual("");
|
|
228
|
+
});
|
|
229
|
+
});
|
|
230
|
+
});
|