Add asynchronous table pagination to v2.17.5
This commit is contained in:
1 parent
86b9609ea7
commit
ab4a40ccd8
5 files changed
+300
-52
No files matched your search
@@ -4,10 +4,15 @@ All notable changes to this project will be documented in this file.
|
|||||||
|
|
||||||
## 2.17.5 - 2026-10-23
|
## 2.17.5 - 2026-10-23
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- Table pagination updates results without reloading the page, preserves search and sorting, and supports browser Back and Forward navigation.
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Added query-aligned database indexes for playlist loading, template and slide lookups, session cleanup, background task claiming, and data-source list ordering.
|
- Added query-aligned database indexes for playlist loading, template and slide lookups, session cleanup, background task claiming, and data-source list ordering.
|
||||||
- Audit log filtering, sorting, and pagination run in SQL; CSV exports still include all matching events.
|
- Audit log filtering, sorting, and pagination run in SQL; CSV exports still include all matching events.
|
||||||
|
- Table header Create buttons use the same right inset as pagination controls.
|
||||||
|
|
||||||
## 2.17.4 - 2026-10-23
|
## 2.17.4 - 2026-10-23
|
||||||
|
|
||||||
|
|||||||
@@ -599,6 +599,7 @@
|
|||||||
|
|
||||||
[data-table-search-container] > .card-header:has(> .card-tools) > .card-tools {
|
[data-table-search-container] > .card-header:has(> .card-tools) > .card-tools {
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
margin-right: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Template animation preview controls */
|
/* Template animation preview controls */
|
||||||
|
|||||||
@@ -81,7 +81,9 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
var currentTable = container.querySelector('[data-table-searchable]');
|
var currentTable = container.querySelector('[data-table-searchable]');
|
||||||
var nextContainer = responseDocument.querySelector('[data-table-search-container]') || responseDocument.querySelector('.card');
|
var containers = Array.prototype.slice.call(document.querySelectorAll('[data-table-search-container]'));
|
||||||
|
var nextContainers = responseDocument.querySelectorAll('[data-table-search-container]');
|
||||||
|
var nextContainer = nextContainers[containers.indexOf(container)];
|
||||||
var nextTable = nextContainer ? nextContainer.querySelector('[data-table-searchable]') : null;
|
var nextTable = nextContainer ? nextContainer.querySelector('[data-table-searchable]') : null;
|
||||||
var currentFooter = container.querySelector('.card-footer');
|
var currentFooter = container.querySelector('.card-footer');
|
||||||
var nextFooter = nextContainer ? nextContainer.querySelector('.card-footer') : null;
|
var nextFooter = nextContainer ? nextContainer.querySelector('.card-footer') : null;
|
||||||
@@ -104,12 +106,75 @@
|
|||||||
} else if (currentFooter && !nextFooter) {
|
} else if (currentFooter && !nextFooter) {
|
||||||
currentFooter.parentNode.removeChild(currentFooter);
|
currentFooter.parentNode.removeChild(currentFooter);
|
||||||
} else if (!currentFooter && nextFooter) {
|
} else if (!currentFooter && nextFooter) {
|
||||||
currentTable.parentNode.appendChild(nextFooter.cloneNode(true));
|
container.appendChild(nextFooter.cloneNode(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadTableResults(container, nextUrl, historyMode, focusInput) {
|
||||||
|
if (!container) {
|
||||||
|
window.location.assign(nextUrl.toString());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (container._cancelTableSearch) {
|
||||||
|
container._cancelTableSearch();
|
||||||
|
}
|
||||||
|
var sequenceId = (container._tableRequestSequence || 0) + 1;
|
||||||
|
container._tableRequestSequence = sequenceId;
|
||||||
|
container.setAttribute('data-table-search-loading', 'true');
|
||||||
|
container.setAttribute('aria-busy', 'true');
|
||||||
|
|
||||||
|
return fetch(nextUrl.toString(), {
|
||||||
|
method: 'GET',
|
||||||
|
headers: {
|
||||||
|
'Accept': 'text/html, application/xhtml+xml',
|
||||||
|
'X-Requested-With': 'XMLHttpRequest'
|
||||||
|
},
|
||||||
|
credentials: 'same-origin',
|
||||||
|
cache: 'no-store'
|
||||||
|
}).then(function (response) {
|
||||||
|
if (!response.ok || (response.redirected && new URL(response.url).pathname !== nextUrl.pathname)) {
|
||||||
|
throw new Error('Unable to load table results.');
|
||||||
|
}
|
||||||
|
return response.text();
|
||||||
|
}).then(function (text) {
|
||||||
|
if (sequenceId !== container._tableRequestSequence) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var responseDocument = new DOMParser().parseFromString(text || '', 'text/html');
|
||||||
|
if (!replaceTableResults(container, responseDocument)) {
|
||||||
|
throw new Error('Missing table results.');
|
||||||
|
}
|
||||||
|
if (historyMode && window.history && window.history[historyMode]) {
|
||||||
|
window.history[historyMode](window.history.state, document.title, nextUrl.pathname + nextUrl.search + nextUrl.hash);
|
||||||
|
}
|
||||||
|
var input = container.querySelector('[data-table-search]');
|
||||||
|
if (input) {
|
||||||
|
var searchParam = input.getAttribute('data-table-search-param') || 'search';
|
||||||
|
input.value = nextUrl.searchParams.get(searchParam) || '';
|
||||||
|
}
|
||||||
|
rebindTableContainer(container);
|
||||||
|
var scroller = container.querySelector('.table-responsive');
|
||||||
|
if (scroller) {
|
||||||
|
scroller.scrollTop = 0;
|
||||||
|
}
|
||||||
|
if (focusInput) {
|
||||||
|
focusSearchInput(input);
|
||||||
|
}
|
||||||
|
}).catch(function () {
|
||||||
|
if (sequenceId === container._tableRequestSequence) {
|
||||||
|
window.location.assign(nextUrl.toString());
|
||||||
|
}
|
||||||
|
}).finally(function () {
|
||||||
|
if (sequenceId === container._tableRequestSequence) {
|
||||||
|
container.removeAttribute('data-table-search-loading');
|
||||||
|
container.removeAttribute('aria-busy');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function initTableSearches(root) {
|
function initTableSearches(root) {
|
||||||
var scope = root && root.querySelectorAll ? root : document;
|
var scope = root && root.querySelectorAll ? root : document;
|
||||||
|
|
||||||
@@ -123,11 +188,15 @@
|
|||||||
var searchParam = String(input.getAttribute('data-table-search-param') || 'search').trim() || 'search';
|
var searchParam = String(input.getAttribute('data-table-search-param') || 'search').trim() || 'search';
|
||||||
var currentUrl = new URL(window.location.href);
|
var currentUrl = new URL(window.location.href);
|
||||||
var pendingSearchTimer = null;
|
var pendingSearchTimer = null;
|
||||||
var requestSequence = 0;
|
|
||||||
var pendingSearchRequests = 0;
|
|
||||||
var container = input.closest('[data-table-search-container]') || input.closest('.card') || null;
|
var container = input.closest('[data-table-search-container]') || input.closest('.card') || null;
|
||||||
|
|
||||||
input.value = String(currentUrl.searchParams.get(searchParam) || '').trim();
|
input.value = String(currentUrl.searchParams.get(searchParam) || '').trim();
|
||||||
|
if (container) {
|
||||||
|
container._cancelTableSearch = function () {
|
||||||
|
window.clearTimeout(pendingSearchTimer);
|
||||||
|
pendingSearchTimer = null;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
var searchGroup = input.closest('.table-search-group');
|
var searchGroup = input.closest('.table-search-group');
|
||||||
if (searchGroup) {
|
if (searchGroup) {
|
||||||
@@ -160,57 +229,13 @@
|
|||||||
}
|
}
|
||||||
nextUrl.searchParams.delete('page');
|
nextUrl.searchParams.delete('page');
|
||||||
|
|
||||||
if (nextUrl.toString() === currentUrl.toString()) {
|
loadTableResults(container, nextUrl, 'replaceState', true);
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
requestSequence += 1;
|
|
||||||
var sequenceId = requestSequence;
|
|
||||||
pendingSearchRequests += 1;
|
|
||||||
if (container) {
|
|
||||||
container.setAttribute('data-table-search-loading', 'true');
|
|
||||||
}
|
|
||||||
|
|
||||||
fetch(nextUrl.toString(), {
|
|
||||||
method: 'GET',
|
|
||||||
headers: {
|
|
||||||
'Accept': 'text/html, application/xhtml+xml',
|
|
||||||
'X-Requested-With': 'XMLHttpRequest'
|
|
||||||
},
|
|
||||||
credentials: 'same-origin',
|
|
||||||
cache: 'no-store'
|
|
||||||
}).then(function (response) {
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Unable to load search results.');
|
|
||||||
}
|
|
||||||
return response.text();
|
|
||||||
}).then(function (text) {
|
|
||||||
if (sequenceId !== requestSequence) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var responseDocument = new DOMParser().parseFromString(text || '', 'text/html');
|
|
||||||
|
|
||||||
if (window.history && window.history.replaceState) {
|
|
||||||
window.history.replaceState({}, document.title, nextUrl.pathname + nextUrl.search + nextUrl.hash);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (replaceTableResults(container, responseDocument)) {
|
|
||||||
currentUrl = nextUrl;
|
|
||||||
rebindTableContainer(container || document);
|
|
||||||
focusSearchInput(input);
|
|
||||||
}
|
|
||||||
}).catch(function () {
|
|
||||||
window.location.assign(nextUrl.toString());
|
|
||||||
}).finally(function () {
|
|
||||||
pendingSearchRequests = Math.max(0, pendingSearchRequests - 1);
|
|
||||||
if (container && pendingSearchRequests === 0) {
|
|
||||||
container.removeAttribute('data-table-search-loading');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function scheduleSearchUpdate() {
|
function scheduleSearchUpdate() {
|
||||||
|
if (container) {
|
||||||
|
container._tableRequestSequence = (container._tableRequestSequence || 0) + 1;
|
||||||
|
}
|
||||||
if (pendingSearchTimer) {
|
if (pendingSearchTimer) {
|
||||||
window.clearTimeout(pendingSearchTimer);
|
window.clearTimeout(pendingSearchTimer);
|
||||||
}
|
}
|
||||||
@@ -239,6 +264,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (window.addEventListener) {
|
if (window.addEventListener) {
|
||||||
|
window.addEventListener('popstate', function () {
|
||||||
|
Array.prototype.forEach.call(document.querySelectorAll('[data-table-search-container]'), function (container) {
|
||||||
|
loadTableResults(container, new URL(window.location.href));
|
||||||
|
});
|
||||||
|
});
|
||||||
window.addEventListener('resize', function () {
|
window.addEventListener('resize', function () {
|
||||||
syncTablePaginationCards(document);
|
syncTablePaginationCards(document);
|
||||||
});
|
});
|
||||||
@@ -247,6 +277,31 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (document.addEventListener) {
|
||||||
|
document.addEventListener('click', function (event) {
|
||||||
|
var link = event.target.closest && event.target.closest('.pagination a.page-link');
|
||||||
|
if (!link || event.defaultPrevented || event.button !== 0 || event.ctrlKey || event.metaKey || event.shiftKey || event.altKey || link.hasAttribute('download') || (link.target && link.target !== '_self')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var container = link.closest('[data-table-search-container]');
|
||||||
|
if (!container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (link.closest('.disabled, .active')) {
|
||||||
|
event.preventDefault();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
var nextUrl = new URL(link.href, window.location.href);
|
||||||
|
var currentUrl = new URL(window.location.href);
|
||||||
|
if (nextUrl.origin !== currentUrl.origin || nextUrl.pathname !== currentUrl.pathname) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
loadTableResults(container, nextUrl, 'pushState');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.loadTableResults = loadTableResults;
|
||||||
window.initTableSearches = initTableSearches;
|
window.initTableSearches = initTableSearches;
|
||||||
window.replaceTableResults = replaceTableResults;
|
window.replaceTableResults = replaceTableResults;
|
||||||
syncTablePaginationCards(document);
|
syncTablePaginationCards(document);
|
||||||
|
|||||||
@@ -103,6 +103,10 @@
|
|||||||
function refreshSortedTable(table, nextUrl, requestSequence, sequenceId) {
|
function refreshSortedTable(table, nextUrl, requestSequence, sequenceId) {
|
||||||
var container = table ? (table.closest('[data-table-search-container]') || table.closest('.card') || null) : null;
|
var container = table ? (table.closest('[data-table-search-container]') || table.closest('.card') || null) : null;
|
||||||
|
|
||||||
|
if (typeof window.loadTableResults === 'function') {
|
||||||
|
return window.loadTableResults(container, nextUrl, 'replaceState');
|
||||||
|
}
|
||||||
|
|
||||||
fetch(nextUrl.toString(), {
|
fetch(nextUrl.toString(), {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -30,6 +30,13 @@ test('standalone slide thumbnail columns shrink to their content and keep thumbn
|
|||||||
assert.match(cellRule, /white-space:\s*nowrap;/);
|
assert.match(cellRule, /white-space:\s*nowrap;/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('table header actions retain the same right inset as the pagination footer', () => {
|
||||||
|
const stylesheet = fs.readFileSync(path.join(__dirname, '..', 'src', 'web', 'public', 'css', 'theme-custom.css'), 'utf8');
|
||||||
|
const toolsRule = stylesheet.match(/\[data-table-search-container\] > \.card-header:has\(> \.card-tools\) > \.card-tools\s*\{([^}]+)\}/)[1];
|
||||||
|
|
||||||
|
assert.match(toolsRule, /margin-right:\s*0;/);
|
||||||
|
});
|
||||||
|
|
||||||
test('mobile responsive tables keep descriptive columns wide enough for phrases', () => {
|
test('mobile responsive tables keep descriptive columns wide enough for phrases', () => {
|
||||||
const stylesheet = fs.readFileSync(path.join(__dirname, '..', 'src', 'web', 'public', 'css', 'theme-custom.css'), 'utf8');
|
const stylesheet = fs.readFileSync(path.join(__dirname, '..', 'src', 'web', 'public', 'css', 'theme-custom.css'), 'utf8');
|
||||||
const mobileRule = stylesheet.match(/@media \(max-width: 767\.98px\)\s*\{\s*\.table-responsive > table\.table > tbody > tr > td:is\(([\s\S]*?)\)\s*\{([^}]+)\}/);
|
const mobileRule = stylesheet.match(/@media \(max-width: 767\.98px\)\s*\{\s*\.table-responsive > table\.table > tbody > tr > td:is\(([\s\S]*?)\)\s*\{([^}]+)\}/);
|
||||||
@@ -119,3 +126,179 @@ test('table pagination cards keep the minimum height for tall content', () => {
|
|||||||
assert.equal(cardStyle.properties['--table-pagination-card-max-height'], '320px');
|
assert.equal(cardStyle.properties['--table-pagination-card-max-height'], '320px');
|
||||||
assert.deepEqual(bodyClassList.toggled, { name: 'table-pagination-page', value: true });
|
assert.deepEqual(bodyClassList.toggled, { name: 'table-pagination-page', value: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function createNavigationHarness() {
|
||||||
|
const events = {};
|
||||||
|
const requests = [];
|
||||||
|
const navigations = [];
|
||||||
|
const history = [];
|
||||||
|
const body = { outerHTML: 'original rows' };
|
||||||
|
const footer = { outerHTML: 'original footer' };
|
||||||
|
const attributes = {};
|
||||||
|
const inputEvents = {};
|
||||||
|
const scroller = { scrollTop: 90 };
|
||||||
|
const input = {
|
||||||
|
value: '',
|
||||||
|
getAttribute(name) { return attributes[name] || null; },
|
||||||
|
setAttribute(name, value) { attributes[name] = value; },
|
||||||
|
closest(selector) { return selector === '[data-table-search-container]' ? container : null; },
|
||||||
|
addEventListener(name, handler) { inputEvents[name] = handler; },
|
||||||
|
focus() {}
|
||||||
|
};
|
||||||
|
const table = { tBodies: [body] };
|
||||||
|
const container = {
|
||||||
|
querySelector(selector) {
|
||||||
|
return {
|
||||||
|
'[data-table-searchable]': table,
|
||||||
|
'.card-footer': footer,
|
||||||
|
'[data-table-search]': input,
|
||||||
|
'.table-responsive': scroller
|
||||||
|
}[selector] || null;
|
||||||
|
},
|
||||||
|
querySelectorAll(selector) { return selector === '[data-table-search]' ? [input] : []; },
|
||||||
|
setAttribute(name, value) { attributes[name] = value; },
|
||||||
|
removeAttribute(name) { delete attributes[name]; }
|
||||||
|
};
|
||||||
|
const sandbox = {
|
||||||
|
URL,
|
||||||
|
document: {
|
||||||
|
title: 'Tables',
|
||||||
|
querySelector() { return null; },
|
||||||
|
querySelectorAll(selector) { return selector === '[data-table-search-container]' ? [container] : []; },
|
||||||
|
addEventListener(name, handler) { events[name] = handler; }
|
||||||
|
},
|
||||||
|
DOMParser: class {
|
||||||
|
parseFromString(text) {
|
||||||
|
const nextContainer = {
|
||||||
|
querySelector(selector) {
|
||||||
|
if (selector === '[data-table-searchable]') return { tBodies: [{ outerHTML: text + ' rows' }] };
|
||||||
|
if (selector === '.card-footer') return { outerHTML: text + ' footer' };
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
querySelector() { return text === 'missing' ? null : nextContainer; },
|
||||||
|
querySelectorAll() { return text === 'missing' ? [] : [nextContainer]; }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
fetch(url, options) {
|
||||||
|
return new Promise((resolve, reject) => requests.push({ url, options, resolve, reject }));
|
||||||
|
},
|
||||||
|
window: {
|
||||||
|
location: { href: 'http://localhost/slides?search=demo&sort=name', assign(url) { navigations.push(url); } },
|
||||||
|
history: { state: { preserved: true } },
|
||||||
|
addEventListener(name, handler) { events[name] = handler; },
|
||||||
|
clearTimeout() {},
|
||||||
|
setTimeout(handler) { events.timer = handler; return 1; }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
for (const method of ['pushState', 'replaceState']) {
|
||||||
|
sandbox.window.history[method] = (state, title, url) => {
|
||||||
|
history.push({ method, state, url });
|
||||||
|
sandbox.window.location.href = new URL(url, sandbox.window.location.href).href;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const scriptPath = path.join(__dirname, '..', 'src', 'web', 'public', 'js', 'table', 'table-search.js');
|
||||||
|
vm.runInNewContext(fs.readFileSync(scriptPath, 'utf8'), sandbox);
|
||||||
|
sandbox.window.initTableSearches(container);
|
||||||
|
|
||||||
|
function click(options = {}) {
|
||||||
|
const link = {
|
||||||
|
href: 'http://localhost/slides?search=demo&sort=name&page=2',
|
||||||
|
hasAttribute() { return false; },
|
||||||
|
closest(selector) {
|
||||||
|
if (selector === '[data-table-search-container]') return container;
|
||||||
|
return options.disabled ? {} : null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const event = {
|
||||||
|
button: 0,
|
||||||
|
target: { closest() { return link; } },
|
||||||
|
preventDefault() { this.defaultPrevented = true; },
|
||||||
|
...options
|
||||||
|
};
|
||||||
|
events.click(event);
|
||||||
|
return event;
|
||||||
|
}
|
||||||
|
async function respond(index, text, ok = true) {
|
||||||
|
requests[index].resolve({ ok, text: async () => text });
|
||||||
|
await new Promise(resolve => setImmediate(resolve));
|
||||||
|
}
|
||||||
|
return { sandbox, container, events, requests, navigations, history, body, footer, attributes, input, inputEvents, scroller, click, respond };
|
||||||
|
}
|
||||||
|
|
||||||
|
test('pagination replaces only table results and pushes a URL preserving search and sort', async () => {
|
||||||
|
const harness = createNavigationHarness();
|
||||||
|
assert.equal(harness.click().defaultPrevented, true);
|
||||||
|
assert.equal(harness.attributes['aria-busy'], 'true');
|
||||||
|
assert.equal(harness.requests[0].options.headers['X-Requested-With'], 'XMLHttpRequest');
|
||||||
|
await harness.respond(0, 'page two');
|
||||||
|
assert.equal(harness.body.outerHTML, 'page two rows');
|
||||||
|
assert.equal(harness.footer.outerHTML, 'page two footer');
|
||||||
|
assert.equal(harness.history[0].method, 'pushState');
|
||||||
|
assert.equal(harness.history[0].url, '/slides?search=demo&sort=name&page=2');
|
||||||
|
assert.equal(harness.history[0].state.preserved, true);
|
||||||
|
assert.equal(harness.attributes['aria-busy'], undefined);
|
||||||
|
assert.equal(harness.scroller.scrollTop, 0);
|
||||||
|
assert.deepEqual(harness.navigations, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pagination leaves modified clicks native and ignores disabled links', () => {
|
||||||
|
const harness = createNavigationHarness();
|
||||||
|
assert.equal(harness.click({ ctrlKey: true }).defaultPrevented, undefined);
|
||||||
|
assert.equal(harness.click({ button: 1 }).defaultPrevented, undefined);
|
||||||
|
assert.equal(harness.click({ disabled: true }).defaultPrevented, true);
|
||||||
|
assert.equal(harness.requests.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Back and Forward fetch the URL table state without adding history entries', async () => {
|
||||||
|
const harness = createNavigationHarness();
|
||||||
|
harness.sandbox.window.location.href = 'http://localhost/slides?search=other&page=3';
|
||||||
|
harness.events.popstate();
|
||||||
|
await harness.respond(0, 'restored');
|
||||||
|
assert.equal(harness.body.outerHTML, 'restored rows');
|
||||||
|
assert.equal(harness.input.value, 'other');
|
||||||
|
assert.equal(harness.history.length, 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('a newer request wins over slower results and stale failures', async () => {
|
||||||
|
const harness = createNavigationHarness();
|
||||||
|
harness.click();
|
||||||
|
harness.events.popstate();
|
||||||
|
await harness.respond(1, 'newest');
|
||||||
|
await harness.respond(0, 'old');
|
||||||
|
assert.equal(harness.body.outerHTML, 'newest rows');
|
||||||
|
harness.click();
|
||||||
|
harness.events.popstate();
|
||||||
|
await harness.respond(3, 'latest');
|
||||||
|
await harness.respond(2, 'failed', false);
|
||||||
|
assert.equal(harness.body.outerHTML, 'latest rows');
|
||||||
|
assert.deepEqual(harness.navigations, []);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('failed or missing table results fall back to normal navigation without updating history', async () => {
|
||||||
|
for (const response of [{ text: 'error', ok: false }, { text: 'missing', ok: true }]) {
|
||||||
|
const harness = createNavigationHarness();
|
||||||
|
harness.click();
|
||||||
|
await harness.respond(0, response.text, response.ok);
|
||||||
|
assert.equal(harness.navigations[0], harness.requests[0].url);
|
||||||
|
assert.equal(harness.history.length, 0);
|
||||||
|
assert.equal(harness.attributes['aria-busy'], undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('typing invalidates pending pagination and search still resets to page one', async () => {
|
||||||
|
const harness = createNavigationHarness();
|
||||||
|
harness.click();
|
||||||
|
harness.input.value = 'new query';
|
||||||
|
harness.inputEvents.input();
|
||||||
|
await harness.respond(0, 'outdated');
|
||||||
|
assert.equal(harness.body.outerHTML, 'original rows');
|
||||||
|
assert.equal(harness.input.value, 'new query');
|
||||||
|
harness.events.timer();
|
||||||
|
assert.equal(new URL(harness.requests[1].url).searchParams.get('page'), null);
|
||||||
|
await harness.respond(1, 'searched');
|
||||||
|
assert.equal(harness.body.outerHTML, 'searched rows');
|
||||||
|
assert.equal(harness.history[0].method, 'replaceState');
|
||||||
|
});
|
||||||
Reference in new issue
Block a user