fix(widgets): replace old page before showing pagination skeleton
This commit is contained in:
1 parent
985f18c750
commit
4c13ef478c
4 files changed
+189
-29
No files matched your search
@@ -197,7 +197,7 @@ function matchesSelector(node, selector) {
|
||||
if (!matchesCompound(cursor, tokens.pop())) return false;
|
||||
while (tokens.length) {
|
||||
const combinator = tokens.pop();
|
||||
const previous = tokens.pop();
|
||||
const previous = combinator === '>' ? tokens.pop() : combinator;
|
||||
if (combinator === '>') {
|
||||
cursor = cursor.parentNode;
|
||||
if (!cursor || !matchesCompound(cursor, previous)) return false;
|
||||
@@ -345,8 +345,9 @@ function createRuntime({ fetch, pages } = {}) {
|
||||
}
|
||||
const requests = [];
|
||||
const config = { restUrl: '/wp-json/aksrt/v1', i18n: { error: 'error', retry: 'retry' } };
|
||||
const fetchStub = fetch || ((url) => {
|
||||
const fetchStub = (url) => {
|
||||
requests.push(url);
|
||||
if (fetch) return fetch(url);
|
||||
const page = Number(new URL(url, 'https://example.test').searchParams.get('page'));
|
||||
const segment = new URL(url, 'https://example.test').searchParams.get('segment');
|
||||
const response = (pages && pages[`${segment}:${page}`]) || {
|
||||
@@ -355,7 +356,7 @@ function createRuntime({ fetch, pages } = {}) {
|
||||
paginate_html: page < 3 ? `<div class="ajax-pag"><a data-next-page="${page + 1}">more</a></div>` : '',
|
||||
};
|
||||
return Promise.resolve({ ok: true, json: () => Promise.resolve(response) });
|
||||
});
|
||||
};
|
||||
const window = {
|
||||
aksrtWidgets: config,
|
||||
matchMedia: () => ({ matches: false }),
|
||||
@@ -380,6 +381,155 @@ function createRuntime({ fetch, pages } = {}) {
|
||||
const tick = () => new Promise((resolve) => setImmediate(resolve));
|
||||
const pageRequests = (requests) => requests.map((url) => Number(new URL(url, 'https://example.test').searchParams.get('page')));
|
||||
|
||||
function pendingFetch() {
|
||||
const pending = [];
|
||||
return {
|
||||
pending,
|
||||
fetch(url) {
|
||||
return new Promise((resolve, reject) => pending.push({
|
||||
url,
|
||||
resolve(data) { resolve({ ok: true, json: () => Promise.resolve(data) }); },
|
||||
reject,
|
||||
}));
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function addPageLink(document, pager, page) {
|
||||
const link = document.createElement('a');
|
||||
link.className = 'page-numbers';
|
||||
link.setAttribute('data-widget-page', String(page));
|
||||
pager.querySelector('.ajax-pag').appendChild(link);
|
||||
return link;
|
||||
}
|
||||
|
||||
function assertOnlyLoader(pager, loader, message) {
|
||||
assert.equal(pager.children.length, 1, message);
|
||||
assert.equal(pager.firstChild, loader);
|
||||
}
|
||||
|
||||
test('numbered pagination replaces the old page with its skeleton before the response arrives', async () => {
|
||||
const transport = pendingFetch();
|
||||
const runtime = createRuntime({ fetch: transport.fetch });
|
||||
const pager = runtime.widget.querySelector('#panel-one [data-widget-items]');
|
||||
const originalPost = pager.querySelector('[data-post-id="post-one"]');
|
||||
const loader = runtime.document.createElement('div');
|
||||
loader.className = 'post_ajax_loader';
|
||||
pager.appendChild(loader);
|
||||
|
||||
addPageLink(runtime.document, pager, 2).click();
|
||||
|
||||
assert.equal(pager.querySelectorAll('[data-post-id]').length, 0, 'old rows must stop occupying space while the replacement is pending');
|
||||
assert.equal(originalPost.parentNode, null);
|
||||
assert.equal(pager.querySelector('.ajax-pag'), null, 'old page controls leave the loading page');
|
||||
assertOnlyLoader(pager, loader);
|
||||
assert.equal(loader.classList.contains('is-loading'), true);
|
||||
assert.equal(pager.getAttribute('aria-busy'), 'true');
|
||||
assert.equal(pager.dataset.page, '1', 'the current page changes only after a successful response');
|
||||
|
||||
transport.pending[0].resolve({
|
||||
html: '<posts class="posts-mini ajax-item" data-post-id="post-two">Page two</posts>',
|
||||
page: 2,
|
||||
paginate_html: '<div class="ajax-pag"><a class="page-numbers" data-widget-page="3">3</a></div>',
|
||||
});
|
||||
await tick();
|
||||
|
||||
assert.deepEqual(pager.querySelectorAll('[data-post-id]').map((node) => node.dataset.postId), ['post-two']);
|
||||
assert.equal(pager.querySelector('.post_ajax_loader'), loader);
|
||||
assert.equal(loader.classList.contains('is-loading'), false);
|
||||
assert.equal(pager.getAttribute('aria-busy'), 'false');
|
||||
assert.equal(pager.dataset.page, '2');
|
||||
|
||||
pager.querySelector('[data-widget-page="3"]').click();
|
||||
assertOnlyLoader(pager, loader, 'the next replacement reuses the same skeleton in the cleared page');
|
||||
assert.equal(loader.classList.contains('is-loading'), true);
|
||||
transport.pending[1].resolve({ html: '', page: 3, paginate_html: '' });
|
||||
await tick();
|
||||
assert.equal(pager.dataset.page, '3');
|
||||
assert.equal(loader.classList.contains('is-loading'), false);
|
||||
});
|
||||
|
||||
test('a failed numbered page restores the old content and retries the requested replacement', async () => {
|
||||
const transport = pendingFetch();
|
||||
const runtime = createRuntime({ fetch: transport.fetch });
|
||||
const pager = runtime.widget.querySelector('#panel-one [data-widget-items]');
|
||||
const originalPost = pager.querySelector('[data-post-id="post-one"]');
|
||||
const originalPagination = pager.querySelector('.ajax-pag');
|
||||
const link = addPageLink(runtime.document, pager, 2);
|
||||
link.click();
|
||||
const loader = pager.querySelector('.post_ajax_loader');
|
||||
link.click();
|
||||
assert.deepEqual(pageRequests(runtime.requests), [2], 'a detached trigger cannot start a duplicate request');
|
||||
transport.pending[0].reject(new Error('temporary failure'));
|
||||
await tick();
|
||||
|
||||
assert.equal(pager.querySelector('[data-post-id="post-one"]'), originalPost, 'failure restores the original row node');
|
||||
assert.equal(pager.querySelector('.ajax-pag'), originalPagination, 'failure restores the original pagination controls');
|
||||
assert.equal(pager.dataset.page, '1');
|
||||
assert.equal(pager.getAttribute('aria-busy'), 'false');
|
||||
assert.equal(loader.classList.contains('is-loading'), false);
|
||||
const retry = pager.querySelector('[data-widget-retry]');
|
||||
assert.equal(retry.dataset.retryPage, '2');
|
||||
assert.equal(retry.dataset.retryAppend, '0');
|
||||
|
||||
retry.click();
|
||||
assert.deepEqual(pageRequests(runtime.requests), [2, 2]);
|
||||
assertOnlyLoader(pager, loader, 'retry clears the restored page before showing its replacement skeleton');
|
||||
transport.pending[1].resolve({
|
||||
html: '<posts class="posts-mini ajax-item" data-post-id="post-two">Page two</posts>',
|
||||
page: 2,
|
||||
paginate_html: '',
|
||||
});
|
||||
await tick();
|
||||
assert.deepEqual(pager.querySelectorAll('[data-post-id]').map((node) => node.dataset.postId), ['post-two']);
|
||||
assert.equal(pager.querySelector('[data-widget-retry]'), null);
|
||||
assert.equal(pager.dataset.page, '2');
|
||||
});
|
||||
|
||||
test('load-more keeps existing rows and places its pending skeleton after them', async () => {
|
||||
const transport = pendingFetch();
|
||||
const runtime = createRuntime({ fetch: transport.fetch });
|
||||
const pager = runtime.widget.querySelector('#panel-one [data-widget-items]');
|
||||
const originalPost = pager.querySelector('[data-post-id="post-one"]');
|
||||
pager.querySelector('[data-next-page="2"]').click();
|
||||
const loader = pager.querySelector('.post_ajax_loader');
|
||||
|
||||
assert.equal(pager.querySelector('[data-post-id="post-one"]'), originalPost);
|
||||
assert.equal(pager.children.indexOf(loader), pager.children.indexOf(originalPost) + 1);
|
||||
assert.equal(loader.classList.contains('is-loading'), true);
|
||||
transport.pending[0].resolve({
|
||||
html: '<posts class="posts-mini ajax-item" data-post-id="post-two">Page two</posts>',
|
||||
page: 2,
|
||||
paginate_html: '',
|
||||
});
|
||||
await tick();
|
||||
assert.deepEqual(pager.querySelectorAll('[data-post-id]').map((node) => node.dataset.postId), ['post-one', 'post-two']);
|
||||
assert.equal(pager.querySelector('.post_ajax_loader'), loader);
|
||||
assert.equal(pager.children.indexOf(loader), 2);
|
||||
assert.equal(loader.classList.contains('is-loading'), false);
|
||||
});
|
||||
|
||||
test('jump pagination clears the page during loading and keeps the skeleton scoped to that tab', async () => {
|
||||
const transport = pendingFetch();
|
||||
const runtime = createRuntime({ fetch: transport.fetch });
|
||||
const pager = runtime.widget.querySelector('#panel-one [data-widget-items]');
|
||||
const otherPager = runtime.widget.querySelector('#panel-two [data-widget-items]');
|
||||
const otherChildren = otherPager.children.slice();
|
||||
const { button } = addJumpControl(runtime.document, pager, { value: '4' });
|
||||
button.click();
|
||||
const loader = pager.querySelector('.post_ajax_loader');
|
||||
|
||||
assertOnlyLoader(pager, loader);
|
||||
assert.equal(loader.classList.contains('is-loading'), true);
|
||||
assert.equal(otherPager.children.length, otherChildren.length);
|
||||
otherChildren.forEach((child, index) => assert.equal(otherPager.children[index], child, 'another tab keeps its existing DOM'));
|
||||
assert.equal(otherPager.querySelector('.is-loading'), null);
|
||||
transport.pending[0].resolve({ html: '<posts class="posts-mini ajax-item" data-post-id="post-four">Page four</posts>', page: 4, paginate_html: '' });
|
||||
await tick();
|
||||
assert.deepEqual(pager.querySelectorAll('[data-post-id]').map((node) => node.dataset.postId), ['post-four']);
|
||||
assert.equal(pager.dataset.page, '4');
|
||||
});
|
||||
|
||||
test('tab panes cache results and keep their current page after switching away and back', async () => {
|
||||
const runtime = createRuntime();
|
||||
const paneOne = runtime.widget.querySelector('#panel-one');
|
||||
@@ -432,7 +582,6 @@ test('overflowing tab centering includes the nav current scroll offset', () => {
|
||||
test('a failed load-more can retry page two and append it without replacing page one', async () => {
|
||||
let pageTwoAttempts = 0;
|
||||
const runtime = createRuntime({ fetch: (url) => {
|
||||
runtime.requests.push(url);
|
||||
const page = Number(new URL(url, 'https://example.test').searchParams.get('page'));
|
||||
if (page === 2 && pageTwoAttempts++ === 0) return Promise.reject(new Error('temporary failure'));
|
||||
return Promise.resolve({
|
||||
|
||||
Reference in new issue
Block a user