mirror of
https://github.com/bilawalsidhu/gods-eye-view.git
synced 2026-11-12 14:22:21 +00:00
fix(ghostmaps): keep age-window changes consistent and make the age choice a switch
Review fixes on top of #1528: - Narrowing the age window only removes markers; a product loaded with a narrower window is reloaded when the window widens again. Fixes markers missing after 7d -> 1d -> 7d and avoids reparsing every KMZ on narrowing. - A wider window over the same edition keeps the open report card. - Date-only event dates cover their whole UTC day, so 1d still shows yesterday's reports. - The age options are one labelled "Event age" radio switch (row `segmented` control, as FIRMS confidence) instead of toggle chips mixed with the product chips. - Un-nest the per-product age test; QA compares data-source counts with the live defaults instead of a hard-coded 2. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
7673351ce0
commit
c4298c4450
17 files changed
+247
-135
No files matched your search
@@ -3471,5 +3471,6 @@
|
||||
"All markers": "كل العلامات",
|
||||
"Show markers of every age": "عرض العلامات من جميع الأعمار",
|
||||
"Maximum event age: {days}d; undated markers are kept": "الحد الأقصى لعمر الحدث: {days} يوم؛ تُحفظ العلامات غير المؤرخة",
|
||||
"Event age": "عمر الحدث",
|
||||
"Event age: {days}d · undated kept": "عمر الحدث: {days} يوم · الاحتفاظ بغير المؤرخ"
|
||||
}
|
||||
@@ -3171,5 +3171,6 @@
|
||||
"All markers": "Alle Markierungen",
|
||||
"Show markers of every age": "Markierungen jeden Alters anzeigen",
|
||||
"Maximum event age: {days}d; undated markers are kept": "Maximales Ereignisalter: {days} Tage; undatierte Markierungen bleiben",
|
||||
"Event age": "Ereignisalter",
|
||||
"Event age: {days}d · undated kept": "Ereignisalter: {days} Tage · undatierte bleiben"
|
||||
}
|
||||
@@ -3246,5 +3246,6 @@
|
||||
"All markers": "Todos los marcadores",
|
||||
"Show markers of every age": "Mostrar marcadores de cualquier antigüedad",
|
||||
"Maximum event age: {days}d; undated markers are kept": "Antigüedad máxima: {days} días; se conservan los marcadores sin fecha",
|
||||
"Event age": "Antigüedad",
|
||||
"Event age: {days}d · undated kept": "Antigüedad: {days} días · se conservan los marcadores sin fecha"
|
||||
}
|
||||
@@ -3246,5 +3246,6 @@
|
||||
"All markers": "Tous les marqueurs",
|
||||
"Show markers of every age": "Afficher les marqueurs de tout âge",
|
||||
"Maximum event age: {days}d; undated markers are kept": "Ancienneté maximale : {days} j ; marqueurs sans date conservés",
|
||||
"Event age": "Ancienneté",
|
||||
"Event age: {days}d · undated kept": "Ancienneté : {days} j · marqueurs sans date conservés"
|
||||
}
|
||||
@@ -3246,5 +3246,6 @@
|
||||
"All markers": "Tutti gli indicatori",
|
||||
"Show markers of every age": "Mostra gli indicatori di qualsiasi età",
|
||||
"Maximum event age: {days}d; undated markers are kept": "Età massima degli eventi: {days} giorni; indicatori senza data mantenuti",
|
||||
"Event age": "Età degli eventi",
|
||||
"Event age: {days}d · undated kept": "Età degli eventi: {days} giorni · senza data mantenuti"
|
||||
}
|
||||
@@ -3096,5 +3096,6 @@
|
||||
"All markers": "すべてのマーカー",
|
||||
"Show markers of every age": "日付にかかわらずすべてのマーカーを表示",
|
||||
"Maximum event age: {days}d; undated markers are kept": "イベントの最大経過日数:{days}日。日付のないマーカーは保持",
|
||||
"Event age": "経過日数",
|
||||
"Event age: {days}d · undated kept": "経過日数:{days}日以内 · 日付なしは保持"
|
||||
}
|
||||
@@ -3246,5 +3246,6 @@
|
||||
"All markers": "Todos os marcadores",
|
||||
"Show markers of every age": "Mostrar marcadores de qualquer idade",
|
||||
"Maximum event age: {days}d; undated markers are kept": "Idade máxima do evento: {days} dias; marcadores sem data mantidos",
|
||||
"Event age": "Idade do evento",
|
||||
"Event age: {days}d · undated kept": "Idade do evento: {days} dias · sem data mantidos"
|
||||
}
|
||||
@@ -3321,5 +3321,6 @@
|
||||
"All markers": "Все метки",
|
||||
"Show markers of every age": "Показать метки любого возраста",
|
||||
"Maximum event age: {days}d; undated markers are kept": "Максимальный возраст события: {days} дн.; метки без даты сохранены",
|
||||
"Event age": "Возраст события",
|
||||
"Event age: {days}d · undated kept": "Возраст события: {days} дн. · метки без даты сохранены"
|
||||
}
|
||||
@@ -3321,5 +3321,6 @@
|
||||
"All markers": "Усі позначки",
|
||||
"Show markers of every age": "Показати позначки будь-якого віку",
|
||||
"Maximum event age: {days}d; undated markers are kept": "Максимальний вік події: {days} дн.; позначки без дати збережено",
|
||||
"Event age": "Вік події",
|
||||
"Event age: {days}d · undated kept": "Вік події: {days} дн. · позначки без дати збережено"
|
||||
}
|
||||
@@ -3096,5 +3096,6 @@
|
||||
"All markers": "所有标记",
|
||||
"Show markers of every age": "显示所有时间的标记",
|
||||
"Maximum event age: {days}d; undated markers are kept": "事件最长保留时间:{days}天;保留无日期标记",
|
||||
"Event age": "事件时间",
|
||||
"Event age: {days}d · undated kept": "事件时间:{days}天内 · 保留无日期标记"
|
||||
}
|
||||
@@ -3096,5 +3096,6 @@
|
||||
"All markers": "所有標記",
|
||||
"Show markers of every age": "顯示所有時間的標記",
|
||||
"Maximum event age: {days}d; undated markers are kept": "事件最長保留時間:{days}天;保留無日期標記",
|
||||
"Event age": "事件時間",
|
||||
"Event age: {days}d · undated kept": "事件時間:{days}天內 · 保留無日期標記"
|
||||
}
|
||||
@@ -208,13 +208,13 @@ export function createGhostMapsLayer({
|
||||
return (_catalog?.products || []).filter(wanted).map(({ id }) => id);
|
||||
}
|
||||
|
||||
function unload(id) {
|
||||
function unload(id, keepSelection = false) {
|
||||
const item = _loaded.get(id);
|
||||
if (!item) return;
|
||||
_loaded.delete(id);
|
||||
_viewer?.dataSources?.remove(item.dataSource, true);
|
||||
item.urls.forEach((url) => URL.revokeObjectURL(url));
|
||||
if (_selectedKey?.startsWith(`${id}:`)) clearSelection();
|
||||
if (!keepSelection && _selectedKey?.startsWith(`${id}:`)) clearSelection();
|
||||
}
|
||||
|
||||
// Remove expired entities, rather than hiding them: horizon culling must
|
||||
@@ -423,8 +423,9 @@ export function createGhostMapsLayer({
|
||||
// Cull before the first frame so far-side pins never flash.
|
||||
_horizon.update(next.pins);
|
||||
await _viewer.dataSources.add(next.dataSource);
|
||||
// The new edition is on the globe before the old one leaves.
|
||||
unload(product.id);
|
||||
// The new edition is on the globe before the old one leaves. A wider
|
||||
// window over the same edition keeps indices, so the card stays.
|
||||
unload(product.id, loaded?.version === next.version);
|
||||
_loaded.set(product.id, next);
|
||||
} catch (error) {
|
||||
if (signal.aborted) return;
|
||||
@@ -473,9 +474,16 @@ export function createGhostMapsLayer({
|
||||
/* private window or blocked storage: keep the session choice */
|
||||
}
|
||||
expireEvents();
|
||||
// Narrowing only removes markers. A product loaded with a narrower window
|
||||
// keeps its old ageDays, so sync() restores omitted placemarks from the
|
||||
// cached KMZ even though its upstream version has not changed.
|
||||
for (const item of _loaded.values())
|
||||
if (
|
||||
item.ageDays === null ||
|
||||
(ageDays !== null && item.ageDays >= ageDays)
|
||||
)
|
||||
item.ageDays = ageDays;
|
||||
notifyRow();
|
||||
// A wider window must restore previously omitted placemarks from the
|
||||
// cached KMZ, even though its upstream version has not changed.
|
||||
return sync();
|
||||
}
|
||||
|
||||
@@ -730,8 +738,10 @@ export function createGhostMapsLayer({
|
||||
})
|
||||
: t('Connecting to the S2 GhostMaps repository…');
|
||||
return {
|
||||
chips: [
|
||||
...GHOSTMAPS_AGE_OPTIONS.map((days) => ({
|
||||
segmented: {
|
||||
id: 'age',
|
||||
label: t('Event age'),
|
||||
options: GHOSTMAPS_AGE_OPTIONS.map((days) => ({
|
||||
id: `age-${days ?? 'all'}`,
|
||||
label:
|
||||
days === null
|
||||
@@ -746,36 +756,36 @@ export function createGhostMapsLayer({
|
||||
active: days === _ageDays,
|
||||
onClick: () => setAgeDays(days),
|
||||
})),
|
||||
...products.map((product) => {
|
||||
const loaded = _loaded.get(product.id);
|
||||
const failed = _failed.get(product.id);
|
||||
return {
|
||||
id: product.id,
|
||||
label: t(PRODUCT_LABELS[product.id] || product.title),
|
||||
active: wanted(product),
|
||||
busy: _loading.has(product.id),
|
||||
state: failed ? 'error' : undefined,
|
||||
color: PRODUCT_COLORS[product.id],
|
||||
title: [
|
||||
product.fileTitle,
|
||||
loaded
|
||||
? tn(
|
||||
loaded.features.size,
|
||||
'{count} placemark',
|
||||
'{count} placemarks',
|
||||
{
|
||||
count: formatNumber(loaded.features.size),
|
||||
},
|
||||
)
|
||||
: null,
|
||||
failed ? t(failed) : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · '),
|
||||
onClick: () => toggleProduct(product.id),
|
||||
};
|
||||
}),
|
||||
],
|
||||
},
|
||||
chips: products.map((product) => {
|
||||
const loaded = _loaded.get(product.id);
|
||||
const failed = _failed.get(product.id);
|
||||
return {
|
||||
id: product.id,
|
||||
label: t(PRODUCT_LABELS[product.id] || product.title),
|
||||
active: wanted(product),
|
||||
busy: _loading.has(product.id),
|
||||
state: failed ? 'error' : undefined,
|
||||
color: PRODUCT_COLORS[product.id],
|
||||
title: [
|
||||
product.fileTitle,
|
||||
loaded
|
||||
? tn(
|
||||
loaded.features.size,
|
||||
'{count} placemark',
|
||||
'{count} placemarks',
|
||||
{
|
||||
count: formatNumber(loaded.features.size),
|
||||
},
|
||||
)
|
||||
: null,
|
||||
failed ? t(failed) : null,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(' · '),
|
||||
onClick: () => toggleProduct(product.id),
|
||||
};
|
||||
}),
|
||||
legend: [],
|
||||
info: `${_ageDays === null ? t('All markers') : t('Event age: {days}d · undated kept', { days: formatNumber(_ageDays) })} · ${info}`,
|
||||
infoTitle: t(
|
||||
|
||||
@@ -203,7 +203,9 @@ const dated = (name, value, label = 'Date') => ({
|
||||
});
|
||||
|
||||
const ageChip = (layer, days) =>
|
||||
layer.getRowControls().chips.find(({ id }) => id === `age-${days ?? 'all'}`);
|
||||
layer
|
||||
.getRowControls()
|
||||
.segmented.options.find(({ id }) => id === `age-${days ?? 'all'}`);
|
||||
|
||||
test('age controls default to seven days and widening restores filtered and expired reports', async (t) => {
|
||||
let at = Date.UTC(2026, 10, 3, 12);
|
||||
@@ -233,10 +235,16 @@ test('age controls default to seven days and widening restores filtered and expi
|
||||
await ageChip(layer, null).onClick();
|
||||
assert.equal(layer.getStats().count, 4);
|
||||
assert.equal(mounted.size, 1, 'filter changes never duplicate data sources');
|
||||
const ageChips = layer
|
||||
.getRowControls()
|
||||
.chips.filter(({ id }) => id.startsWith('age-'));
|
||||
assert.equal(ageChips.filter(({ active }) => active).length, 1);
|
||||
const controls = layer.getRowControls();
|
||||
assert.equal(
|
||||
controls.segmented.options.filter(({ active }) => active).length,
|
||||
1,
|
||||
);
|
||||
assert.equal(
|
||||
controls.chips.some(({ id }) => id.startsWith('age-')),
|
||||
false,
|
||||
'age options are one switch, not product chips',
|
||||
);
|
||||
assert.equal(ageChip(layer, null).active, true);
|
||||
});
|
||||
|
||||
@@ -309,49 +317,49 @@ test('a filter changed during a pending load never installs the superseded filte
|
||||
created.length - 1,
|
||||
'discarded load releases icons',
|
||||
);
|
||||
});
|
||||
|
||||
test('age filtering and All markers apply to every conflict-map product, including optional maps', async (t) => {
|
||||
for (const product of [...GHOSTMAPS_PRODUCTS, GHOSTMAPS_LATEST_DROP]) {
|
||||
await t.test(product.id, async (t) => {
|
||||
const { layer, viewer, mounted } = fixture(t, {
|
||||
now: () => Date.UTC(2026, 10, 3, 12),
|
||||
products: [{ ...product, version: 'a'.repeat(40) }],
|
||||
reports: [
|
||||
dated(
|
||||
'Old event',
|
||||
'10/1/2026',
|
||||
product.id === 'cip' ? 'Date of Event' : 'Date',
|
||||
),
|
||||
dated('Recent event', '11/2/2026'),
|
||||
{ name: 'Undated reference feature' },
|
||||
],
|
||||
});
|
||||
await layer.update(viewer);
|
||||
if (!product.defaultOn) {
|
||||
layer
|
||||
.getRowControls()
|
||||
.chips.find(({ id }) => id === product.id)
|
||||
.onClick();
|
||||
await layer.update(viewer);
|
||||
}
|
||||
assert.deepEqual(layer.getStats().products, [product.id]);
|
||||
assert.equal(layer.getStats().count, 2);
|
||||
assert.deepEqual(
|
||||
layer.getAnalystRecords().map(({ name }) => name),
|
||||
['Recent event', 'Undated reference feature'],
|
||||
);
|
||||
await ageChip(layer, null).onClick();
|
||||
assert.equal(
|
||||
layer.getStats().count,
|
||||
3,
|
||||
"All markers restores this product's old events",
|
||||
);
|
||||
await ageChip(layer, 7).onClick();
|
||||
assert.equal(layer.getStats().count, 2);
|
||||
assert.equal(mounted.size, 1);
|
||||
test('age filtering and All markers apply to every conflict-map product, including optional maps', async (t) => {
|
||||
for (const product of [...GHOSTMAPS_PRODUCTS, GHOSTMAPS_LATEST_DROP]) {
|
||||
await t.test(product.id, async (t) => {
|
||||
const { layer, viewer, mounted } = fixture(t, {
|
||||
now: () => Date.UTC(2026, 10, 3, 12),
|
||||
products: [{ ...product, version: 'a'.repeat(40) }],
|
||||
reports: [
|
||||
dated(
|
||||
'Old event',
|
||||
'10/1/2026',
|
||||
product.id === 'cip' ? 'Date of Event' : 'Date',
|
||||
),
|
||||
dated('Recent event', '11/2/2026'),
|
||||
{ name: 'Undated reference feature' },
|
||||
],
|
||||
});
|
||||
}
|
||||
});
|
||||
await layer.update(viewer);
|
||||
if (!product.defaultOn) {
|
||||
layer
|
||||
.getRowControls()
|
||||
.chips.find(({ id }) => id === product.id)
|
||||
.onClick();
|
||||
await layer.update(viewer);
|
||||
}
|
||||
assert.deepEqual(layer.getStats().products, [product.id]);
|
||||
assert.equal(layer.getStats().count, 2);
|
||||
assert.deepEqual(
|
||||
layer.getAnalystRecords().map(({ name }) => name),
|
||||
['Recent event', 'Undated reference feature'],
|
||||
);
|
||||
await ageChip(layer, null).onClick();
|
||||
assert.equal(
|
||||
layer.getStats().count,
|
||||
3,
|
||||
"All markers restores this product's old events",
|
||||
);
|
||||
await ageChip(layer, 7).onClick();
|
||||
assert.equal(layer.getStats().count, 2);
|
||||
assert.equal(mounted.size, 1);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
test('default products filter old event dates while keeping recent, future and undated markers', async (t) => {
|
||||
@@ -448,7 +456,8 @@ test('unchanged cached products age out on the timer, clear selected cards and k
|
||||
|
||||
test('expiry removes every geometry and polygon outline belonging to an old report', async (t) => {
|
||||
t.mock.timers.enable({ apis: ['setInterval'] });
|
||||
let at = Date.UTC(2026, 10, 3);
|
||||
// 10/27 is a whole UTC day: at 7d it expires when 11/3 ends.
|
||||
let at = Date.UTC(2026, 10, 4) - 30_000;
|
||||
const polygon = element('Polygon', [
|
||||
element('outerBoundaryIs', [
|
||||
element('LinearRing', [
|
||||
@@ -477,3 +486,75 @@ test('expiry removes every geometry and polygon outline belonging to an old repo
|
||||
assert.equal(ds.entities.values.length, 0);
|
||||
assert.equal(layer.getStats().count, 0);
|
||||
});
|
||||
|
||||
test('narrowing then returning to the previous window restores its reports without a duplicate source', async (t) => {
|
||||
const { layer, viewer, mounted } = fixture(t, {
|
||||
now: () => Date.UTC(2026, 10, 3, 12),
|
||||
reports: [dated('Today', '11/3/2026'), dated('Three days', '10/31/2026')],
|
||||
});
|
||||
await layer.update(viewer);
|
||||
assert.equal(layer.getStats().count, 2);
|
||||
await ageChip(layer, 1).onClick();
|
||||
assert.equal(layer.getStats().count, 1);
|
||||
await ageChip(layer, 7).onClick();
|
||||
await layer.update(viewer);
|
||||
assert.equal(layer.getStats().count, 2, '7d must show both reports again');
|
||||
assert.equal(mounted.size, 1);
|
||||
});
|
||||
|
||||
test('two quick clicks, 1d then 7d, restore the report', async (t) => {
|
||||
const at = Date.UTC(2026, 10, 3, 12);
|
||||
const { layer, viewer } = fixture(t, {
|
||||
now: () => at,
|
||||
reports: [dated('Today', '11/3/2026'), dated('Three days', '10/31/2026')],
|
||||
});
|
||||
await layer.update(viewer);
|
||||
const a = ageChip(layer, 1).onClick();
|
||||
const b = ageChip(layer, 7).onClick();
|
||||
await a;
|
||||
await b;
|
||||
await layer.update(viewer);
|
||||
assert.equal(layer.getStats().count, 2);
|
||||
});
|
||||
|
||||
test('narrowing the window keeps the open card of a surviving marker', async (t) => {
|
||||
let click;
|
||||
const cards = [];
|
||||
const { layer, viewer } = fixture(t, {
|
||||
now: () => Date.UTC(2026, 10, 3, 12),
|
||||
reports: [dated('Recent', '11/2/2026'), dated('Older', '10/20/2026')],
|
||||
controls: {
|
||||
overlayHost: { setEntries: (_id, entries) => cards.push(entries) },
|
||||
screenSpaceEventHandlerFactory: () => ({
|
||||
setInputAction: (fn) => {
|
||||
click = fn;
|
||||
},
|
||||
destroy() {},
|
||||
}),
|
||||
picking: { resolvePickId: (picked) => picked },
|
||||
},
|
||||
});
|
||||
await layer.update(viewer);
|
||||
await ageChip(layer, 30).onClick();
|
||||
assert.equal(layer.getStats().count, 2);
|
||||
viewer.scene.pick = () => 'ghostmaps:cip:0:0';
|
||||
click({ position: { x: 0, y: 0 } });
|
||||
assert.equal(cards.at(-1).length, 1);
|
||||
await ageChip(layer, 3).onClick();
|
||||
assert.equal(layer.getStats().count, 1);
|
||||
assert.equal(
|
||||
cards.at(-1).length,
|
||||
1,
|
||||
'card of the surviving marker stays open',
|
||||
);
|
||||
});
|
||||
|
||||
test('a report dated yesterday is shown by 1d one hour into today (UTC)', async (t) => {
|
||||
const { layer, viewer } = fixture(t, {
|
||||
now: () => Date.UTC(2026, 10, 6, 1),
|
||||
reports: [dated('Yesterday', '11/5/2026')],
|
||||
});
|
||||
await layer.update(viewer);
|
||||
await ageChip(layer, 1).onClick();
|
||||
assert.equal(layer.getStats().count, 1);
|
||||
});
|
||||
@@ -73,7 +73,9 @@ export const GHOSTMAPS_DEFAULT_AGE_DAYS = 7;
|
||||
export const GHOSTMAPS_AGE_OPTIONS = Object.freeze([1, 3, 7, 14, 30, null]);
|
||||
|
||||
/** ArcGIS US dates (optionally with a time) and ISO dates, interpreted in UTC
|
||||
* when no zone is supplied. Reject calendar rollovers instead of guessing. */
|
||||
* when no zone is supplied. A date without a time means the whole UTC day, so
|
||||
* it resolves to the day's last millisecond: "1d" still shows yesterday's
|
||||
* reports. Reject calendar rollovers instead of guessing. */
|
||||
export function parseGhostMapsEventDate(value) {
|
||||
const input = String(value ?? '').trim();
|
||||
const us =
|
||||
@@ -113,7 +115,7 @@ export function parseGhostMapsEventDate(value) {
|
||||
const parsed = Date.parse(zoned);
|
||||
return Number.isFinite(parsed) ? parsed : null;
|
||||
}
|
||||
return at;
|
||||
return us?.[4] ? at : at + 86400_000 - 1;
|
||||
}
|
||||
|
||||
/** The event's date, never its report/upload date or dates mentioned in prose. */
|
||||
|
||||
@@ -14,15 +14,15 @@ import {
|
||||
|
||||
test('event dates parse ArcGIS calendar dates and timestamps consistently in UTC', () => {
|
||||
for (const [value, iso] of [
|
||||
['10/28/2026', '2026-10-28T00:00:00Z'],
|
||||
['10/28/2026', '2026-10-28T23:59:59.999Z'],
|
||||
['10/28/2026 6:12:00 PM', '2026-10-28T18:12:00Z'],
|
||||
['1/17/2026 9:46:41 AM', '2026-01-17T09:46:41Z'],
|
||||
['10/28/2026 12:00 AM', '2026-10-28T00:00:00Z'],
|
||||
['10/28/2026 12:00 PM', '2026-10-28T12:00:00Z'],
|
||||
['2026-10-28', '2026-10-28T00:00:00Z'],
|
||||
['2026-10-28', '2026-10-28T23:59:59.999Z'],
|
||||
['2026-10-28T12:34:56', '2026-10-28T12:34:56Z'],
|
||||
['2026-10-28T12:34:56.123-04:00', '2026-10-28T16:34:56.123Z'],
|
||||
['2/29/2024', '2024-02-29T00:00:00Z'],
|
||||
['2/29/2024', '2024-02-29T23:59:59.999Z'],
|
||||
])
|
||||
assert.equal(parseGhostMapsEventDate(value), Date.parse(iso), value);
|
||||
for (const value of [
|
||||
@@ -52,7 +52,7 @@ test('event date fields take precedence over report dates and prose, even after
|
||||
row('Date', '10/30/2026') +
|
||||
row('Date of Event', '10/28/2026')
|
||||
}</table></td></tr></table>`;
|
||||
assert.equal(ghostMapsEventDate(html), Date.UTC(2026, 9, 28));
|
||||
assert.equal(ghostMapsEventDate(html), Date.UTC(2026, 9, 29) - 1);
|
||||
assert.equal(
|
||||
ghostMapsEventDate(
|
||||
`<table>${row('Date', '<Null>')}${row('Date Of Report', '11/2/2026')}</table>`,
|
||||
@@ -61,7 +61,7 @@ test('event date fields take precedence over report dates and prose, even after
|
||||
);
|
||||
assert.equal(
|
||||
ghostMapsEventDate(`<table>${row('Date_Of_Event', '10/28/2026')}</table>`),
|
||||
Date.UTC(2026, 9, 28),
|
||||
Date.UTC(2026, 9, 29) - 1,
|
||||
);
|
||||
assert.equal(ghostMapsEventDate('Occurred 10/28/2026'), null);
|
||||
assert.equal(
|
||||
|
||||
Reference in new issue
Block a user