mirror of
https://github.com/ZoneMinder/zoneminder.git
synced 2026-09-24 03:25:34 -04:00
Applying a named filter and clicking LIST MATCHES landed on Events showing "No matching records found" until a manual refresh. Two independent defects produce that, and both are fixed here. Stored filter selections overriding the applied filter ------------------------------------------------------ The zmFilter_* cookies remember what was last selected in console, montage and montagereview. They are a convenience for the default, filter-less page, but they were also applied on top of a filter the request had specified, silently widening or narrowing it: the applied filter was ANDed with a date range left over from an earlier visit to another view, so it matched nothing. Refreshing appeared to fix it because the bar's reset control blanks those inputs. The cause was a layering one. Filter::simple_widget() refilled any empty term value from that term's cookie as it rendered the input, so it overrode the value addTerm() had been given, from below the layer that knows what the request asked for. An earlier attempt to fix this in events.php could not hold for that reason: it blanked the values and they were refilled during rendering, and ajaxRequest() sends the live inputs rather than the URL. Filter now resolves nothing. It renders the value it was handed, and the six cookie fallbacks are gone; terms keep their cookie name so edits still persist client-side. The callers that build those terms decide instead, next to getFilterSelection(), which already resolved request-before-cookie this way. Each resolves its value into a local before the addTerm() calls, testing $use_stored in the same statement that reads the cookie, so the rule is visible at the point it applies. montagereview needs a window to draw whatever happens, so with a filter present it derives one from the filter's own terms and otherwise defaults to the last hour, rather than reaching for the stored window; its Notes term no longer seeds from a cookie inside the branch that already has an explicit filter. Tables never retrying a request skipped while hidden ---------------------------------------------------- The table views skip their ajax request while the page is hidden so a background tab does not poll. Bootstrap-table calls that function on init as well as on refresh, though, and a skipped request was never re-issued: the table rendered "No matching records found" over a result it never asked for, with nothing to bring it back. A page can be hidden for the whole of its load - opened in a background tab, restored, or behind another window - and the same guard is in seven views: events, console, log, frames, reports, snapshots and watch. deferTableRequestWhileHidden() skips the request as before and records the table, so it is refreshed the moment the page becomes visible. The queue is drained before refreshing, because refresh() calls the ajax function synchronously and would otherwise re-add a table that is still hidden. Tests ----- tests/audit-filter-cookies.php checks both halves of the first rule: that Filter resolves nothing, and that every stored-selection read in a caller tests $use_stored. It works a statement at a time, joining continuation lines, since a value and its guard often span a line break. Anything wider is too coarse: the enclosing block holds other guarded reads and would mask one that lost its own. tests/js/table-helpers.test.js covers the deferral queue, including a table re-deferred during its own refresh. Verified against a live instance with the stale cookies still set: a "last hour" named filter queried 0 of 4 matching events before and 12 of 12 after; the filter-less page still restores the stored date range; and a table deferred while hidden repaints on becoming visible.
139 lines
4.7 KiB
JavaScript
139 lines
4.7 KiB
JavaScript
'use strict';
|
|
|
|
const assert = require('assert');
|
|
const path = require('path');
|
|
|
|
let passed = 0;
|
|
let failed = 0;
|
|
function test(name, fn) {
|
|
try {
|
|
fn();
|
|
console.log(' ok ' + name);
|
|
passed++;
|
|
} catch (e) {
|
|
console.error(' FAIL ' + name);
|
|
console.error(' ' + e.message);
|
|
failed++;
|
|
}
|
|
}
|
|
|
|
// Minimal stand-ins for the browser globals table-helpers.js touches. document
|
|
// has to exist before the module is required, because it registers the
|
|
// visibilitychange listener at load.
|
|
const listeners = {};
|
|
global.document = {
|
|
visibilityState: 'visible',
|
|
addEventListener: function(name, fn) {
|
|
(listeners[name] = listeners[name] || []).push(fn);
|
|
},
|
|
};
|
|
function fireVisibilityChange() {
|
|
(listeners['visibilitychange'] || []).forEach((fn) => fn());
|
|
}
|
|
|
|
const ZM = require(path.join(__dirname, '../../web/js/table-helpers.js'));
|
|
|
|
// A stand-in for a bootstrap-table jQuery object, recording the calls made.
|
|
function fakeTable() {
|
|
const calls = [];
|
|
return {
|
|
calls: calls,
|
|
bootstrapTable: function(action) {
|
|
calls.push(action);
|
|
},
|
|
};
|
|
}
|
|
function reset() {
|
|
ZM.tablesPendingVisibility.length = 0;
|
|
global.document.visibilityState = 'visible';
|
|
}
|
|
|
|
console.log('deferTableRequestWhileHidden');
|
|
test('does not defer while the page is visible', () => {
|
|
reset();
|
|
const t = fakeTable();
|
|
assert.strictEqual(ZM.deferTableRequestWhileHidden(t), false);
|
|
assert.deepStrictEqual(t.calls, [], 'must not touch the table when visible');
|
|
assert.strictEqual(ZM.tablesPendingVisibility.length, 0);
|
|
});
|
|
test('defers and hides the spinner while hidden', () => {
|
|
reset();
|
|
global.document.visibilityState = 'hidden';
|
|
const t = fakeTable();
|
|
assert.strictEqual(ZM.deferTableRequestWhileHidden(t), true, 'caller must skip its request');
|
|
assert.deepStrictEqual(t.calls, ['hideLoading']);
|
|
assert.strictEqual(ZM.tablesPendingVisibility.length, 1);
|
|
});
|
|
test('records a repeatedly deferred table only once', () => {
|
|
reset();
|
|
global.document.visibilityState = 'hidden';
|
|
const t = fakeTable();
|
|
ZM.deferTableRequestWhileHidden(t);
|
|
ZM.deferTableRequestWhileHidden(t);
|
|
ZM.deferTableRequestWhileHidden(t);
|
|
assert.strictEqual(ZM.tablesPendingVisibility.length, 1, 'auto-refresh ticks must not stack');
|
|
});
|
|
|
|
console.log('refreshTablesPendingVisibility');
|
|
test('refreshes a deferred table when the page becomes visible', () => {
|
|
// The regression: bootstrap-table renders "No matching records found" over the
|
|
// result of a request that was skipped, and without this nothing ever re-issues
|
|
// it, so the table stays empty until a manual refresh (issue #5026).
|
|
reset();
|
|
global.document.visibilityState = 'hidden';
|
|
const t = fakeTable();
|
|
ZM.deferTableRequestWhileHidden(t);
|
|
global.document.visibilityState = 'visible';
|
|
fireVisibilityChange();
|
|
assert.deepStrictEqual(t.calls, ['hideLoading', 'refresh']);
|
|
assert.strictEqual(ZM.tablesPendingVisibility.length, 0, 'queue must drain');
|
|
});
|
|
test('does nothing while still hidden', () => {
|
|
reset();
|
|
global.document.visibilityState = 'hidden';
|
|
const t = fakeTable();
|
|
ZM.deferTableRequestWhileHidden(t);
|
|
fireVisibilityChange();
|
|
assert.deepStrictEqual(t.calls, ['hideLoading'], 'still hidden, nothing to refresh yet');
|
|
assert.strictEqual(ZM.tablesPendingVisibility.length, 1, 'must stay queued');
|
|
});
|
|
test('refreshes every deferred table', () => {
|
|
reset();
|
|
global.document.visibilityState = 'hidden';
|
|
const a = fakeTable();
|
|
const b = fakeTable();
|
|
ZM.deferTableRequestWhileHidden(a);
|
|
ZM.deferTableRequestWhileHidden(b);
|
|
global.document.visibilityState = 'visible';
|
|
fireVisibilityChange();
|
|
assert.deepStrictEqual(a.calls, ['hideLoading', 'refresh']);
|
|
assert.deepStrictEqual(b.calls, ['hideLoading', 'refresh']);
|
|
});
|
|
test('a table re-deferred during refresh stays queued rather than looping', () => {
|
|
// refresh() calls the ajax function synchronously, which defers again if the
|
|
// page went back to hidden. Draining first keeps that from spinning.
|
|
reset();
|
|
global.document.visibilityState = 'hidden';
|
|
const t = fakeTable();
|
|
t.bootstrapTable = function(action) {
|
|
t.calls.push(action);
|
|
if (action === 'refresh') {
|
|
global.document.visibilityState = 'hidden';
|
|
ZM.deferTableRequestWhileHidden(t);
|
|
}
|
|
};
|
|
ZM.deferTableRequestWhileHidden(t);
|
|
global.document.visibilityState = 'visible';
|
|
fireVisibilityChange();
|
|
assert.deepStrictEqual(t.calls, ['hideLoading', 'refresh', 'hideLoading']);
|
|
assert.strictEqual(ZM.tablesPendingVisibility.length, 1, 'queued for the next time it is shown');
|
|
});
|
|
test('is a no-op when nothing was deferred', () => {
|
|
reset();
|
|
fireVisibilityChange();
|
|
assert.strictEqual(ZM.tablesPendingVisibility.length, 0);
|
|
});
|
|
|
|
console.log('\n' + passed + ' passed, ' + failed + ' failed');
|
|
process.exit(failed ? 1 : 0);
|