mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-11 05:42:22 +00:00
Let the News panel narrow the window to 12, 6, 3 or 1 hours
The panel's window label becomes a choice of the server's window and each shorter one. The feed and map filter to it locally; the hotspot counts are read again for it (/api/news/hotspots?hours=N, now on the allowlist). A server names the longest window it serves as maxWindowHours.
This commit is contained in:
1 parent
ee3593cfbd
commit
4817d2a648
22 files changed
+198
-21
No files matched your search
@@ -1,6 +1,7 @@
|
||||
- Add the News layer (Events ▸ News) and its right-rail NEWS panel: the last
|
||||
24 hours from curated hotspots (or the fewer hours a server names, shown in
|
||||
the panel), coloured by severity only, with hotspot
|
||||
24 hours from curated hotspots (or the fewer hours a server names), narrowed
|
||||
in the panel to the last 12, 6, 3 or 1 hours with the hotspot counts read for
|
||||
that window, coloured by severity only, with hotspot
|
||||
beacons, outlines with disputed areas, picture cards for major events, an
|
||||
"In view" list below 3,000 km and a continuous feed whose cards fly the globe
|
||||
to the event. Core serves no news itself: set `GEV_NEWS_ORIGIN` to a news
|
||||
|
||||
@@ -84,6 +84,7 @@
|
||||
"Reading the news…": "جارٍ قراءة الأخبار…",
|
||||
"News, last {hours} hours": "الأخبار، آخر {hours} ساعة",
|
||||
"Last {hours} h": "آخر {hours} ساعة",
|
||||
"Time window": "الفترة الزمنية",
|
||||
"Hotspots": "بؤر ساخنة",
|
||||
"Severity": "الخطورة",
|
||||
"Category": "الفئة",
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
"Reading the news…": "Nachrichten werden gelesen…",
|
||||
"News, last {hours} hours": "Nachrichten der letzten {hours} Stunden",
|
||||
"Last {hours} h": "Letzte {hours} Std.",
|
||||
"Time window": "Zeitraum",
|
||||
"Hotspots": "Brennpunkte",
|
||||
"Severity": "Schweregrad",
|
||||
"Category": "Kategorie",
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
"Reading the news…": "Leyendo las noticias…",
|
||||
"News, last {hours} hours": "Noticias de las últimas {hours} horas",
|
||||
"Last {hours} h": "Últimas {hours} h",
|
||||
"Time window": "Periodo",
|
||||
"Hotspots": "Focos",
|
||||
"Severity": "Gravedad",
|
||||
"Category": "Categoría",
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
"Reading the news…": "Lecture des actualités…",
|
||||
"News, last {hours} hours": "Actualités des dernières {hours} heures",
|
||||
"Last {hours} h": "Dernières {hours} h",
|
||||
"Time window": "Période",
|
||||
"Hotspots": "Foyers",
|
||||
"Severity": "Gravité",
|
||||
"Category": "Catégorie",
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
"Reading the news…": "Lettura delle notizie…",
|
||||
"News, last {hours} hours": "Notizie delle ultime {hours} ore",
|
||||
"Last {hours} h": "Ultime {hours} h",
|
||||
"Time window": "Periodo",
|
||||
"Hotspots": "Focolai",
|
||||
"Severity": "Gravità",
|
||||
"Category": "Categoria",
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
"Reading the news…": "ニュースを読み込み中…",
|
||||
"News, last {hours} hours": "直近{hours}時間のニュース",
|
||||
"Last {hours} h": "直近{hours}時間",
|
||||
"Time window": "期間",
|
||||
"Hotspots": "ホットスポット",
|
||||
"Severity": "深刻度",
|
||||
"Category": "カテゴリ",
|
||||
|
||||
@@ -75,6 +75,7 @@
|
||||
"Reading the news…": "Lendo as notícias…",
|
||||
"News, last {hours} hours": "Notícias das últimas {hours} horas",
|
||||
"Last {hours} h": "Últimas {hours} h",
|
||||
"Time window": "Período",
|
||||
"Hotspots": "Focos",
|
||||
"Severity": "Gravidade",
|
||||
"Category": "Categoria",
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
"Reading the news…": "Читаем новости…",
|
||||
"News, last {hours} hours": "Новости за последние {hours} ч",
|
||||
"Last {hours} h": "За {hours} ч",
|
||||
"Time window": "Период",
|
||||
"Hotspots": "Горячие точки",
|
||||
"Severity": "Тяжесть",
|
||||
"Category": "Категория",
|
||||
|
||||
@@ -78,6 +78,7 @@
|
||||
"Reading the news…": "Читаємо новини…",
|
||||
"News, last {hours} hours": "Новини за останні {hours} год",
|
||||
"Last {hours} h": "За {hours} год",
|
||||
"Time window": "Період",
|
||||
"Hotspots": "Гарячі точки",
|
||||
"Severity": "Тяжкість",
|
||||
"Category": "Категорія",
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
"Reading the news…": "正在读取新闻…",
|
||||
"News, last {hours} hours": "最近 {hours} 小时的新闻",
|
||||
"Last {hours} h": "最近 {hours} 小时",
|
||||
"Time window": "时间范围",
|
||||
"Hotspots": "热点",
|
||||
"Severity": "严重程度",
|
||||
"Category": "类别",
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
"Reading the news…": "正在讀取新聞…",
|
||||
"News, last {hours} hours": "最近 {hours} 小時的新聞",
|
||||
"Last {hours} h": "最近 {hours} 小時",
|
||||
"Time window": "時間範圍",
|
||||
"Hotspots": "熱點",
|
||||
"Severity": "嚴重程度",
|
||||
"Category": "類別",
|
||||
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
markerStyle,
|
||||
mediaLabels,
|
||||
mergeFeedPage,
|
||||
newsWindowChoices,
|
||||
newsWindowMs,
|
||||
normalizeFilters,
|
||||
passesFilters,
|
||||
@@ -142,7 +143,9 @@ export function createNewsLayer({
|
||||
autoPages = 0;
|
||||
let hotspots = [];
|
||||
// The window the server serves (newsWindowMs): a host may serve
|
||||
// fewer hours than the day.
|
||||
// fewer hours than the day. `windowMs` is the one shown: that window, or a
|
||||
// shorter one chosen in the panel (filters.hours).
|
||||
let maxWindowMs = NEWS_WINDOW_MS;
|
||||
let windowMs = NEWS_WINDOW_MS;
|
||||
let mapReports = [];
|
||||
let feed = emptyFeed();
|
||||
@@ -165,6 +168,11 @@ export function createNewsLayer({
|
||||
null;
|
||||
const selected = () => (selectedId ? findReport(selectedId) : null);
|
||||
const onMap = (report) => visibleOnMap(report, filters, now(), windowMs);
|
||||
const shownWindowMs = () =>
|
||||
Math.min(maxWindowMs, (filters.hours ?? Infinity) * 3_600_000);
|
||||
// The hours to ask the hotspot counts for, when shorter than the server's.
|
||||
const askedHours = () =>
|
||||
windowMs < maxWindowMs ? windowMs / 3_600_000 : undefined;
|
||||
|
||||
function markerCartesian(report, pin = 0) {
|
||||
const { lat, lon } = reportMarkerPosition(report, pin);
|
||||
@@ -920,7 +928,9 @@ export function createNewsLayer({
|
||||
if (controller.signal.aborted || inViewRequest !== controller) return;
|
||||
inView = {
|
||||
...inView,
|
||||
minor: page.reports.filter((r) => withinNewsWindow(r, now(), windowMs)),
|
||||
minor: page.reports.filter((r) =>
|
||||
withinNewsWindow(r, now(), maxWindowMs),
|
||||
),
|
||||
loading: false,
|
||||
error: null,
|
||||
};
|
||||
@@ -1045,6 +1055,7 @@ export function createNewsLayer({
|
||||
error: error || feed.error ? t(error || feed.error) : null,
|
||||
lastRunAt,
|
||||
windowHours: windowMs / 3_600_000,
|
||||
windowChoices: newsWindowChoices(maxWindowMs / 3_600_000),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1139,7 +1150,10 @@ export function createNewsLayer({
|
||||
notify();
|
||||
try {
|
||||
const [index, todays] = await Promise.all([
|
||||
source.getHotspots({ signal: controller.signal }),
|
||||
source.getHotspots({
|
||||
signal: controller.signal,
|
||||
hours: askedHours(),
|
||||
}),
|
||||
source.getFeed(
|
||||
{
|
||||
hotspot: 'all',
|
||||
@@ -1157,9 +1171,16 @@ export function createNewsLayer({
|
||||
hotspots = index.hotspots;
|
||||
loadOutlines();
|
||||
lastRunAt = index.lastRunAt;
|
||||
windowMs = newsWindowMs(index.windowHours);
|
||||
// A server that names no limit serves the window its counts cover
|
||||
// when asked for none.
|
||||
maxWindowMs = newsWindowMs(
|
||||
index.maxWindowHours ??
|
||||
(askedHours() ? maxWindowMs / 3_600_000 : index.windowHours),
|
||||
);
|
||||
windowMs = shownWindowMs();
|
||||
// Kept for the server's whole window, so a longer choice needs no read.
|
||||
mapReports = todays.reports.filter((r) =>
|
||||
withinNewsWindow(r, now(), windowMs),
|
||||
withinNewsWindow(r, now(), maxWindowMs),
|
||||
);
|
||||
filters = normalizeFilters({}, hotspots, filters);
|
||||
error = null;
|
||||
@@ -1196,6 +1217,8 @@ export function createNewsLayer({
|
||||
const reread = readKey(next) !== readKey(filters);
|
||||
const minorChanged = next.includeMinor !== filters.includeMinor;
|
||||
filters = next;
|
||||
const previousWindowMs = windowMs;
|
||||
windowMs = shownWindowMs();
|
||||
if (
|
||||
Object.hasOwn(params.filters, 'hotspot') &&
|
||||
next.hotspot !== 'all'
|
||||
@@ -1208,6 +1231,8 @@ export function createNewsLayer({
|
||||
render();
|
||||
if (reread) void loadFeed('replace');
|
||||
if (minorChanged) void refreshInView({ force: true });
|
||||
// The hotspot counts are the server's, so a new window reads them again.
|
||||
if (windowMs !== previousWindowMs) void layer.update(viewer);
|
||||
notify();
|
||||
}
|
||||
if (params.loadMore === true) void loadFeed('append');
|
||||
|
||||
@@ -275,6 +275,53 @@ test('In view with minor news reads once, not per pan; a failed read shows', asy
|
||||
assert.deepEqual(view.items, []);
|
||||
});
|
||||
|
||||
test('a shorter window narrows the feed and reads the hotspot counts for it', async (t) => {
|
||||
const asked = [];
|
||||
const { layer } = fixture(t, {
|
||||
source: {
|
||||
getHotspots: async ({ hours } = {}) => {
|
||||
asked.push(hours);
|
||||
return {
|
||||
...hotspotsPayload,
|
||||
windowHours: hours ?? 12,
|
||||
maxWindowHours: 12,
|
||||
};
|
||||
},
|
||||
getFeed: async () => ({
|
||||
reports: [
|
||||
report('recent', { publishedAt: '2026-10-28T09:30:00.000Z' }),
|
||||
report('earlier', { publishedAt: '2026-10-28T05:00:00.000Z' }),
|
||||
],
|
||||
nextBefore: null,
|
||||
}),
|
||||
},
|
||||
});
|
||||
layer.enable();
|
||||
await layer.update();
|
||||
await settle();
|
||||
let view = layer.getRowControls().feed;
|
||||
assert.deepEqual(view.windowChoices, [12, 6, 3, 1]);
|
||||
assert.equal(view.windowHours, 12);
|
||||
assert.deepEqual(
|
||||
view.items.map((item) => item.id),
|
||||
['recent', 'earlier'],
|
||||
);
|
||||
layer.setParams({ filters: { hours: 3 } });
|
||||
view = layer.getRowControls().feed;
|
||||
assert.equal(view.windowHours, 3);
|
||||
assert.deepEqual(
|
||||
view.items.map((item) => item.id),
|
||||
['recent'],
|
||||
);
|
||||
for (let i = 0; i < 3; i++) await settle();
|
||||
assert.deepEqual(asked, [undefined, 3]);
|
||||
// The server's own window still bounds the choices.
|
||||
assert.deepEqual(layer.getRowControls().feed.windowChoices, [12, 6, 3, 1]);
|
||||
// A choice longer than the server serves shows all it serves.
|
||||
layer.setParams({ filters: { hours: 24 } });
|
||||
assert.equal(layer.getRowControls().feed.windowHours, 12);
|
||||
});
|
||||
|
||||
test('a server without news is unavailable once and is not asked again', async (t) => {
|
||||
const failure = Object.assign(
|
||||
new Error('News feed not configured on this server'),
|
||||
|
||||
@@ -21,6 +21,18 @@ export function newsWindowMs(hours) {
|
||||
? hours * 3_600_000
|
||||
: NEWS_WINDOW_MS;
|
||||
}
|
||||
|
||||
/** The windows the News panel offers, longest first. */
|
||||
export const NEWS_WINDOW_CHOICES = Object.freeze([24, 12, 6, 3, 1]);
|
||||
|
||||
/**
|
||||
* The windows offered under a server's own window of `maxHours`: that window
|
||||
* itself, then each shorter choice.
|
||||
*/
|
||||
export function newsWindowChoices(maxHours) {
|
||||
const cap = newsWindowMs(maxHours) / 3_600_000;
|
||||
return [cap, ...NEWS_WINDOW_CHOICES.filter((hours) => hours < cap)];
|
||||
}
|
||||
// A publish time this far ahead of the clock is skew, not a future story
|
||||
// (as data/regionalNewsWindow.js).
|
||||
const NEWS_CLOCK_SKEW_MS = 15 * 60_000;
|
||||
@@ -616,6 +628,8 @@ export function normalizeFilters(next = {}, hotspots = [], previous = {}) {
|
||||
includeMinor: merged.includeMinor === true,
|
||||
// Event cards on the map; off leaves only the clicked event's card.
|
||||
cards: merged.cards !== false,
|
||||
// A shorter window than the server's; null shows all it serves.
|
||||
hours: NEWS_WINDOW_CHOICES.includes(merged.hours) ? merged.hours : null,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
isPressing,
|
||||
markerStyle,
|
||||
mergeFeedPage,
|
||||
newsWindowChoices,
|
||||
newsWindowMs,
|
||||
normalizeFilters,
|
||||
passesFilters,
|
||||
@@ -287,8 +288,11 @@ test('filters default to significant news and map to a last-24-hours query', ()
|
||||
minSeverity: 2,
|
||||
includeMinor: false,
|
||||
cards: true,
|
||||
hours: null,
|
||||
});
|
||||
assert.equal(normalizeFilters({ cards: false }, hotspots).cards, false);
|
||||
assert.equal(normalizeFilters({ hours: 6 }, hotspots).hours, 6);
|
||||
assert.equal(normalizeFilters({ hours: 5 }, hotspots).hours, null);
|
||||
assert.equal(normalizeFilters({ minSeverity: 0 }, hotspots).minSeverity, 0);
|
||||
assert.equal(normalizeFilters({ hotspot: 'gone' }, hotspots).hotspot, 'all');
|
||||
assert.equal(
|
||||
@@ -599,6 +603,21 @@ test('the hotspots answer carries the window it was counted over', () => {
|
||||
normalizeHotspotsPayload({ ...base, windowHours: 48 }).windowHours,
|
||||
null,
|
||||
);
|
||||
const shorter = normalizeHotspotsPayload({
|
||||
...base,
|
||||
windowHours: 3,
|
||||
maxWindowHours: 12,
|
||||
});
|
||||
assert.deepEqual([shorter.windowHours, shorter.maxWindowHours], [3, 12]);
|
||||
assert.equal(normalizeHotspotsPayload(base).maxWindowHours, null);
|
||||
});
|
||||
|
||||
test('the window choices stop at the window the server serves', () => {
|
||||
assert.deepEqual(newsWindowChoices(24), [24, 12, 6, 3, 1]);
|
||||
assert.deepEqual(newsWindowChoices(12), [12, 6, 3, 1]);
|
||||
assert.deepEqual(newsWindowChoices(8), [8, 6, 3, 1]);
|
||||
assert.deepEqual(newsWindowChoices(1), [1]);
|
||||
assert.deepEqual(newsWindowChoices(null), [24, 12, 6, 3, 1]);
|
||||
});
|
||||
|
||||
test('a checked severity keeps saying who set it', () => {
|
||||
|
||||
@@ -95,10 +95,15 @@ function normalizeDeveloping(items) {
|
||||
return out;
|
||||
}
|
||||
|
||||
const hoursOrNull = (hours) =>
|
||||
Number.isInteger(hours) && hours >= 1 && hours <= 24 ? hours : null;
|
||||
|
||||
/**
|
||||
* `/api/news/hotspots` → `{hotspots, categories, lastRunAt, windowHours}`
|
||||
* (`windowHours` null when the server names none), or null when the
|
||||
* payload has no hotspot list.
|
||||
* `/api/news/hotspots` → `{hotspots, categories, lastRunAt, windowHours,
|
||||
* maxWindowHours}`, or null when the payload has no hotspot list.
|
||||
* `windowHours` is the window the counts cover and `maxWindowHours` the
|
||||
* longest the server serves this reader; each is null when the server names
|
||||
* none.
|
||||
*/
|
||||
export function normalizeHotspotsPayload(payload) {
|
||||
if (!Array.isArray(payload?.hotspots)) return null;
|
||||
@@ -136,12 +141,8 @@ export function normalizeHotspotsPayload(payload) {
|
||||
)
|
||||
: [],
|
||||
lastRunAt: iso(payload.ingest?.lastRunAt),
|
||||
windowHours:
|
||||
Number.isInteger(payload.windowHours) &&
|
||||
payload.windowHours >= 1 &&
|
||||
payload.windowHours <= 24
|
||||
? payload.windowHours
|
||||
: null,
|
||||
windowHours: hoursOrNull(payload.windowHours),
|
||||
maxWindowHours: hoursOrNull(payload.maxWindowHours),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,15 @@ export function newsFeedQuery(query = {}) {
|
||||
export function newsQueryAllowed(url) {
|
||||
const params = [...url.searchParams];
|
||||
if (new Set(params.map(([key]) => key)).size !== params.length) return false;
|
||||
if (url.pathname === `${BASE_PATH}/hotspots`) return params.length === 0;
|
||||
if (url.pathname === `${BASE_PATH}/hotspots`)
|
||||
return (
|
||||
params.length === 0 ||
|
||||
(params.length === 1 &&
|
||||
params[0][0] === 'hours' &&
|
||||
/^\d{1,2}$/.test(params[0][1]) &&
|
||||
Number(params[0][1]) >= 1 &&
|
||||
Number(params[0][1]) <= 24)
|
||||
);
|
||||
if (url.pathname === `${BASE_PATH}/outline`) {
|
||||
const keys = params.map(([key]) => key).sort();
|
||||
const value = url.searchParams;
|
||||
@@ -128,9 +136,12 @@ export function createNewsSource({
|
||||
return readResponseJsonCapped(response, MAX_BYTES, request);
|
||||
}
|
||||
return {
|
||||
async getHotspots({ signal } = {}) {
|
||||
// `hours` asks for counts over a shorter window than the server's own.
|
||||
async getHotspots({ signal, hours } = {}) {
|
||||
const payload = normalizeHotspotsPayload(
|
||||
await get('/hotspots', { signal }),
|
||||
await get(hours ? `/hotspots?hours=${hours}` : '/hotspots', {
|
||||
signal,
|
||||
}),
|
||||
);
|
||||
if (!payload) throw new Error(msg('Malformed news hotspots response'));
|
||||
return payload;
|
||||
|
||||
@@ -53,6 +53,7 @@ test('reads validated hotspots, feed and outlines from the same-origin routes',
|
||||
);
|
||||
assert.deepEqual((await source.getOutline('ua')).polygons, []);
|
||||
await source.getCountryOutline('SD');
|
||||
await source.getHotspots({ hours: 6 });
|
||||
assert.deepEqual(
|
||||
calls.map((c) => c.url),
|
||||
[
|
||||
@@ -60,6 +61,7 @@ test('reads validated hotspots, feed and outlines from the same-origin routes',
|
||||
'/api/news/feed?hotspot=all&limit=60&today=1',
|
||||
'/api/news/outline?hotspot=ua&v=4',
|
||||
'/api/news/outline?country=SD&v=4',
|
||||
'/api/news/hotspots?hours=6',
|
||||
],
|
||||
);
|
||||
for (const { url, init } of calls) {
|
||||
@@ -155,7 +157,12 @@ test('the host allowlist admits the client routes and refuses anything else', ()
|
||||
),
|
||||
true,
|
||||
);
|
||||
assert.equal(allowed('/api/news/hotspots?hours=6'), true);
|
||||
for (const path of [
|
||||
'/api/news/hotspots?hours=0',
|
||||
'/api/news/hotspots?hours=25',
|
||||
'/api/news/hotspots?hours=6&hours=3',
|
||||
'/api/news/hotspots?hours=6&x=1',
|
||||
'/api/news/hotspots?x=1',
|
||||
'/api/news/feed?limit=501',
|
||||
'/api/news/feed?limit=0',
|
||||
|
||||
@@ -8,7 +8,8 @@ import RailChips from '../rail/RailChips.vue';
|
||||
import NewsCard from './NewsCard.vue';
|
||||
|
||||
// The News panel: the window the server serves only (the last 24 hours, or
|
||||
// fewer where the host serves less). Hotspot chips, filters, the chosen
|
||||
// fewer where the host serves less), or a shorter one chosen at the
|
||||
// top (24/12/6/3/1 h). Hotspot chips, filters, the chosen
|
||||
// event, "In view" (reports near the view centre while the camera is low)
|
||||
// and a continuous feed, newest first. It renders the `feed` view the news
|
||||
// layer publishes and acts only through `onParams`.
|
||||
@@ -44,6 +45,15 @@ const categoryOptions = computed(() => [
|
||||
{ value: 'all', label: t('All categories') },
|
||||
...(props.view?.categories ?? []),
|
||||
]);
|
||||
const windowOptions = computed(() =>
|
||||
(props.view?.windowChoices ?? []).map((hours) => ({
|
||||
value: String(hours),
|
||||
label: t('Last {hours} h', { hours }),
|
||||
})),
|
||||
);
|
||||
// The longest choice is the server's whole window, kept as no choice.
|
||||
const chooseWindow = (hours) =>
|
||||
setFilter('hours', hours === props.view?.windowChoices?.[0] ? null : hours);
|
||||
const selectedId = computed(() => props.view?.selected?.id ?? null);
|
||||
// The chosen event leads the panel unless the feed already shows it open.
|
||||
const selectedApart = computed(() => {
|
||||
@@ -78,7 +88,15 @@ const clear = () => props.onParams({ clear: true });
|
||||
class="news-readout"
|
||||
:aria-label="t('News, last {hours} hours', { hours: view.windowHours })"
|
||||
>
|
||||
<p class="news-readout__window ui-mono">{{
|
||||
<UiSelect
|
||||
v-if="windowOptions.length > 1"
|
||||
class="gev-field news-filter news-readout__window-choice"
|
||||
:aria-label="t('Time window')"
|
||||
:model-value="String(view.windowHours)"
|
||||
:options="windowOptions"
|
||||
@update:model-value="chooseWindow(Number($event))"
|
||||
/>
|
||||
<p v-else class="news-readout__window ui-mono">{{
|
||||
t('Last {hours} h', { hours: view.windowHours })
|
||||
}}</p>
|
||||
<div class="news-readout__chips" role="group" :aria-label="t('Hotspots')">
|
||||
|
||||
@@ -278,6 +278,24 @@ test('scrolling to the end asks once per page, again after a filter change or a
|
||||
news.destroy();
|
||||
});
|
||||
|
||||
test('the window choice offers shorter windows; the longest is the server window', () => {
|
||||
const { news, panel, calls } = fixture();
|
||||
news.update(view({ windowHours: 12, windowChoices: [12, 6, 3, 1] }));
|
||||
assert.equal(panel.querySelector('.news-readout__window'), null);
|
||||
const choice = panel.querySelector('select[aria-label="Time window"]');
|
||||
assert.deepEqual(
|
||||
[...choice.options].map((option) => option.textContent),
|
||||
['Last 12 h', 'Last 6 h', 'Last 3 h', 'Last 1 h'],
|
||||
);
|
||||
assert.equal(choice.value, '12');
|
||||
choice.value = '3';
|
||||
choice.dispatchEvent(new Event('change'));
|
||||
assert.deepEqual(calls.at(-1)[1], { filters: { hours: 3 } });
|
||||
choice.value = '12';
|
||||
choice.dispatchEvent(new Event('change'));
|
||||
assert.deepEqual(calls.at(-1)[1], { filters: { hours: null } });
|
||||
});
|
||||
|
||||
test('the panel names the window the server serves', () => {
|
||||
const { news, panel } = fixture();
|
||||
news.update(view({ windowHours: 12 }));
|
||||
|
||||
@@ -131,6 +131,11 @@
|
||||
.news-filter {
|
||||
min-width: 0;
|
||||
}
|
||||
/* The window choice sits where the window label does, as narrow as its text. */
|
||||
.news-readout__window-choice {
|
||||
align-self: flex-start;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
.news-toggle {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
Reference in new issue
Block a user