550 lines
25 KiB
JavaScript
550 lines
25 KiB
JavaScript
'use strict';
|
|
|
|
const assert = require('node:assert/strict');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
const vm = require('node:vm');
|
|
const test = require('node:test');
|
|
|
|
const root = path.resolve(__dirname, '..', '..');
|
|
|
|
const { MiniDocument } = require('../helpers/mini-dom.cjs');
|
|
|
|
function tabMarkup(document, { firstPage = 1, firstSegmentLoaded = true } = {}) {
|
|
const root = document.createElement('div');
|
|
root.className = 'widget-tab-post style-mini posts-mini-lists zib-widget';
|
|
root.setAttribute('data-widget', 'tab_posts');
|
|
root.setAttribute('data-instance', 'widget-7');
|
|
const nav = document.createElement('ul');
|
|
nav.className = 'list-inline scroll-x no-scrollbar';
|
|
nav.setAttribute('role', 'tablist');
|
|
nav.clientWidth = 260;
|
|
nav.scrollWidth = 420;
|
|
const tabOne = addTab(document, nav, 0, 'tab-one', 'panel-one', true);
|
|
const tabTwo = addTab(document, nav, 1, 'tab-two', 'panel-two', false);
|
|
nav.rectLeft = 20;
|
|
tabOne.rectLeft = 100;
|
|
tabOne.rectWidth = 100;
|
|
tabTwo.rectLeft = 380;
|
|
tabTwo.rectWidth = 100;
|
|
root.appendChild(nav);
|
|
const content = document.createElement('div');
|
|
content.className = 'tab-content';
|
|
content.appendChild(addPane(document, 'panel-one', 'tab-one', 0, firstPage, firstSegmentLoaded, true));
|
|
content.appendChild(addPane(document, 'panel-two', 'tab-two', 1, 1, false, false));
|
|
root.appendChild(content);
|
|
return { root, nav, tabOne, tabTwo };
|
|
}
|
|
|
|
function addTab(document, nav, segment, id, panelId, selected) {
|
|
const item = document.createElement('li');
|
|
if (selected) item.classList.add('active');
|
|
const tab = document.createElement('a');
|
|
tab.setAttribute('role', 'tab');
|
|
tab.setAttribute('id', id);
|
|
tab.setAttribute('aria-controls', panelId);
|
|
tab.setAttribute('aria-selected', selected ? 'true' : 'false');
|
|
tab.setAttribute('tabindex', selected ? '0' : '-1');
|
|
tab.setAttribute('data-segment', String(segment));
|
|
item.appendChild(tab);
|
|
nav.appendChild(item);
|
|
return tab;
|
|
}
|
|
|
|
function addPane(document, id, tabId, segment, page, loaded, active) {
|
|
const pane = document.createElement('div');
|
|
pane.className = 'tab-pane fade' + (active ? ' active in' : '');
|
|
pane.setAttribute('id', id);
|
|
pane.setAttribute('role', 'tabpanel');
|
|
pane.setAttribute('aria-labelledby', tabId);
|
|
const pager = document.createElement('div');
|
|
pager.className = 'widget-ajaxpager';
|
|
pager.setAttribute('data-widget-items', '');
|
|
pager.setAttribute('data-widget', 'tab_posts');
|
|
pager.setAttribute('data-instance', 'widget-7');
|
|
pager.setAttribute('data-segment', String(segment));
|
|
pager.setAttribute('data-page', String(page));
|
|
if (loaded) pager.setAttribute('data-loaded', '1');
|
|
if (active && loaded) {
|
|
pager.innerHTML = '<posts class="posts-mini ajax-item" data-post-id="post-one"><div class="posts-mini-con"><h2 class="item-heading"><a>Page one</a></h2></div></posts>';
|
|
pager.appendChild(nextPage(document, 2));
|
|
} else {
|
|
const loader = document.createElement('div');
|
|
loader.className = 'post_ajax_loader';
|
|
pager.appendChild(loader);
|
|
}
|
|
pane.appendChild(pager);
|
|
return pane;
|
|
}
|
|
|
|
function nextPage(document, page) {
|
|
const pagination = document.createElement('div');
|
|
pagination.className = 'ajax-pag';
|
|
const link = document.createElement('a');
|
|
link.setAttribute('data-next-page', String(page));
|
|
pagination.appendChild(link);
|
|
return pagination;
|
|
}
|
|
|
|
function addJumpControl(document, pager, { max = 20, value = '4' } = {}) {
|
|
const pagination = document.createElement('div');
|
|
pagination.className = 'ajax-pag';
|
|
const wrapper = document.createElement('span');
|
|
wrapper.className = 'pag-jump';
|
|
const input = document.createElement('input');
|
|
input.setAttribute('data-widget-jump', '');
|
|
input.setAttribute('type', 'text');
|
|
input.max = max;
|
|
input.value = value;
|
|
const button = document.createElement('button');
|
|
button.setAttribute('data-widget-jump-go', '');
|
|
wrapper.appendChild(input);
|
|
wrapper.appendChild(button);
|
|
pagination.appendChild(wrapper);
|
|
pager.appendChild(pagination);
|
|
return { input, button };
|
|
}
|
|
|
|
function postsMarkup(document, unveiled = false) {
|
|
const root = document.createElement('div');
|
|
root.className = 'widget-main-post style-list';
|
|
const pager = document.createElement('div');
|
|
pager.className = 'widget-ajaxpager ' + (unveiled ? 'lazyloaded' : 'lazyload');
|
|
pager.setAttribute('data-widget-items', '');
|
|
pager.setAttribute('data-widget-lazy', '');
|
|
pager.setAttribute('data-widget', 'posts');
|
|
pager.setAttribute('data-instance', 'aksrt_widget_posts-2');
|
|
pager.setAttribute('data-page', '1');
|
|
pager.setAttribute('aria-busy', 'true');
|
|
pager.innerHTML = '<div class="post_ajax_loader is-loading"><div class="posts-item placeholder">Loading</div></div>';
|
|
root.appendChild(pager);
|
|
return { root };
|
|
}
|
|
|
|
function createRuntime({ fetch, pages, posts = false, unveiled = false, restUrl = '/wp-json/aksrt/v1/widgets/', observe = false } = {}) {
|
|
const document = new MiniDocument();
|
|
const { root: widget, nav, tabOne, tabTwo } = posts ? postsMarkup(document, unveiled) : tabMarkup(document);
|
|
document.body.appendChild(widget);
|
|
const autoFun = [];
|
|
function jQuery() {
|
|
return {
|
|
on(event, callback) { if (event === 'auto_fun') autoFun.push(callback); return this; },
|
|
trigger(event) { if (event === 'auto_fun') autoFun.forEach((callback) => callback()); return this; },
|
|
};
|
|
}
|
|
const requests = [];
|
|
const config = { restUrl, i18n: { error: 'error', retry: 'retry' } };
|
|
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}`]) || {
|
|
html: `<posts class="posts-mini ajax-item" data-post-id="post-${segment}-${page}"><div class="posts-mini-con"><h2 class="item-heading"><a>Returned ${segment}/${page}</a></h2></div></posts>`,
|
|
page,
|
|
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) });
|
|
};
|
|
let notifyMutation;
|
|
const Observer = observe ? class {
|
|
constructor(callback) { notifyMutation = callback; }
|
|
observe() {}
|
|
} : undefined;
|
|
const window = {
|
|
aksrtWidgets: config,
|
|
location: { href: 'https://example.test/' },
|
|
matchMedia: () => ({ matches: false }),
|
|
jQuery,
|
|
};
|
|
if (observe) window.MutationObserver = Observer;
|
|
vm.runInNewContext(fs.readFileSync(path.join(root, 'assets/widgets/frontend.js'), 'utf8'), {
|
|
window,
|
|
document,
|
|
fetch: fetchStub,
|
|
Promise,
|
|
Array,
|
|
Number,
|
|
String,
|
|
Object,
|
|
Math,
|
|
encodeURIComponent,
|
|
URL,
|
|
MutationObserver: Observer,
|
|
});
|
|
return { document, widget, nav, tabOne, tabTwo, requests, autoFun, notifyMutation };
|
|
}
|
|
|
|
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');
|
|
const paneTwo = runtime.widget.querySelector('#panel-two');
|
|
const pagerOne = paneOne.querySelector('[data-widget-items]');
|
|
const pagerTwo = paneTwo.querySelector('[data-widget-items]');
|
|
runtime.tabTwo.click();
|
|
await tick();
|
|
assert.deepEqual(pageRequests(runtime.requests), [1]);
|
|
const pageTwoMore = pagerTwo.querySelector('[data-next-page="2"]');
|
|
assert.ok(pageTwoMore, 'the returned REST markup is parsed and inserted into the selected pane');
|
|
pageTwoMore.click();
|
|
await tick();
|
|
assert.deepEqual(pageRequests(runtime.requests), [1, 2]);
|
|
assert.equal(pagerTwo.dataset.page, '2');
|
|
|
|
runtime.tabOne.click();
|
|
runtime.tabTwo.click();
|
|
await tick();
|
|
assert.deepEqual(pageRequests(runtime.requests), [1, 2], 'a cached pane does not issue another first-page request');
|
|
assert.equal(pagerOne.dataset.page, '1', 'switching tabs preserves the initial pane page');
|
|
assert.equal(pagerTwo.dataset.page, '2', 'switching back preserves the loaded pane page');
|
|
assert.equal(pagerTwo.querySelectorAll('[data-post-id]').length, 2, 'returned page markup remains in the cached pane');
|
|
});
|
|
|
|
test('arrow keys move tab focus, activate the adjacent pane, and center an overflowing nav item', async () => {
|
|
const runtime = createRuntime();
|
|
const keyEvent = runtime.tabOne.dispatch('keydown', { key: 'ArrowRight' });
|
|
await tick();
|
|
|
|
assert.equal(keyEvent.defaultPrevented, true);
|
|
assert.equal(runtime.document.activeElement, runtime.tabTwo);
|
|
assert.equal(runtime.tabTwo.getAttribute('aria-selected'), 'true');
|
|
assert.equal(runtime.widget.querySelector('#panel-two').classList.contains('active'), true);
|
|
assert.equal(runtime.nav.scrollCalls.length, 1);
|
|
assert.equal(runtime.nav.scrollCalls[0].left, 280);
|
|
assert.equal(runtime.nav.scrollCalls[0].behavior, 'smooth');
|
|
assert.deepEqual(pageRequests(runtime.requests), [1]);
|
|
});
|
|
|
|
test('overflowing tab centering includes the nav current scroll offset', () => {
|
|
const runtime = createRuntime();
|
|
runtime.nav.scrollLeft = 35;
|
|
runtime.tabTwo.click();
|
|
|
|
assert.equal(runtime.nav.scrollCalls.length, 1);
|
|
assert.equal(runtime.nav.scrollCalls[0].left, 315, 'the target uses viewport bounds plus current nav scrollLeft');
|
|
});
|
|
|
|
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) => {
|
|
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({
|
|
ok: true,
|
|
json: () => Promise.resolve({
|
|
html: '<posts class="posts-mini ajax-item" data-post-id="post-two"><h2 class="item-heading"><a>Page two</a></h2></posts>',
|
|
page,
|
|
paginate_html: '',
|
|
}),
|
|
});
|
|
} });
|
|
const pager = runtime.widget.querySelector('#panel-one [data-widget-items]');
|
|
|
|
pager.querySelector('[data-next-page="2"]').click();
|
|
await tick();
|
|
const retry = pager.querySelector('[data-widget-retry]');
|
|
assert.ok(retry, 'a failed append exposes the runtime retry control');
|
|
assert.equal(retry.dataset.retryPage, '2');
|
|
assert.equal(retry.dataset.retryAppend, '1');
|
|
|
|
retry.click();
|
|
await tick();
|
|
assert.deepEqual(pageRequests(runtime.requests), [2, 2], 'retry repeats the failed page rather than resetting to page one');
|
|
assert.deepEqual(pager.querySelectorAll('[data-post-id]').map((node) => node.dataset.postId), ['post-one', 'post-two']);
|
|
assert.equal(pager.dataset.page, '2');
|
|
assert.equal(runtime.autoFun.length, 1, 'successful markup insertion initializes widgets through auto_fun');
|
|
});
|
|
|
|
test('jump button replaces the selected pane, keeps its loader, and stops the legacy click handler', async () => {
|
|
const runtime = createRuntime();
|
|
const pager = runtime.widget.querySelector('#panel-one [data-widget-items]');
|
|
const loader = runtime.document.createElement('div');
|
|
loader.className = 'post_ajax_loader';
|
|
pager.insertBefore(loader, pager.querySelector('.ajax-pag'));
|
|
const { button } = addJumpControl(runtime.document, pager, { value: '4' });
|
|
let legacyClicks = 0;
|
|
runtime.document.body.addEventListener('click', () => { legacyClicks += 1; });
|
|
|
|
const click = button.dispatch('click');
|
|
await tick();
|
|
|
|
assert.equal(click.defaultPrevented, true);
|
|
assert.equal(click.propagationStopped, true);
|
|
assert.equal(legacyClicks, 0, 'the legacy pag_jump_go document handler cannot process the same click');
|
|
assert.deepEqual(pageRequests(runtime.requests), [4]);
|
|
assert.equal(pager.dataset.page, '4');
|
|
assert.equal(pager.querySelector('[data-post-id="post-0-4"]') !== null, true, 'the returned page markup replaces the active pane items');
|
|
assert.equal(pager.querySelector('[data-post-id="post-one"]'), null);
|
|
assert.equal(pager.querySelector('.post_ajax_loader'), loader, 'replacement preserves the loader node for subsequent requests');
|
|
});
|
|
|
|
test('Enter submits a valid jump and stops the legacy key handler', async () => {
|
|
const runtime = createRuntime();
|
|
const pager = runtime.widget.querySelector('#panel-one [data-widget-items]');
|
|
const loader = runtime.document.createElement('div');
|
|
loader.className = 'post_ajax_loader';
|
|
pager.insertBefore(loader, pager.querySelector('.ajax-pag'));
|
|
const { input } = addJumpControl(runtime.document, pager, { value: '5' });
|
|
let legacyKeydowns = 0;
|
|
runtime.document.body.addEventListener('keydown', () => { legacyKeydowns += 1; });
|
|
|
|
const keydown = input.dispatch('keydown', { key: 'Enter' });
|
|
await tick();
|
|
|
|
assert.equal(keydown.defaultPrevented, true);
|
|
assert.equal(keydown.propagationStopped, true);
|
|
assert.equal(legacyKeydowns, 0, 'the legacy pag_jump_go key handler cannot submit twice');
|
|
assert.deepEqual(pageRequests(runtime.requests), [5]);
|
|
assert.equal(pager.dataset.page, '5');
|
|
assert.ok(pager.querySelector('[data-post-id="post-0-5"]'));
|
|
assert.equal(pager.querySelector('.post_ajax_loader'), loader);
|
|
});
|
|
|
|
test('jump input clamps pages to max and rejects invalid or current pages without fetching', async () => {
|
|
const runtime = createRuntime();
|
|
const pager = runtime.widget.querySelector('#panel-one [data-widget-items]');
|
|
const { input } = addJumpControl(runtime.document, pager, { max: 8, value: '99' });
|
|
|
|
const inputEvent = input.dispatch('input');
|
|
assert.equal(input.value, '8');
|
|
assert.equal(inputEvent.propagationStopped, true);
|
|
|
|
input.value = 'not-a-page';
|
|
input.dispatch('input');
|
|
assert.equal(input.value, '');
|
|
input.dispatch('keydown', { key: 'Enter' });
|
|
assert.equal(runtime.requests.length, 0, 'invalid input does not request a page');
|
|
|
|
input.value = '1';
|
|
const currentPage = input.dispatch('keydown', { key: 'Enter' });
|
|
await tick();
|
|
assert.equal(currentPage.defaultPrevented, true);
|
|
assert.equal(runtime.requests.length, 0, 'submitting the already selected page does not request again');
|
|
assert.equal(runtime.document.activeElement, input, 'invalid and current page values return focus to the input');
|
|
});
|
|
|
|
test('article widget waits for its own lazy reveal and fetches the first page once', async () => {
|
|
const transport = pendingFetch();
|
|
const runtime = createRuntime({ posts: true, fetch: transport.fetch });
|
|
const pager = runtime.widget.querySelector('[data-widget-items]');
|
|
assert.equal(runtime.requests.length, 0, 'offscreen widgets keep their placeholder without fetching');
|
|
assert.ok(pager.querySelector('.post_ajax_loader.is-loading'));
|
|
pager.querySelector('.placeholder').dispatch('lazybeforeunveil');
|
|
assert.equal(runtime.requests.length, 0, 'a descendant image reveal does not load the pager');
|
|
pager.dispatch('lazybeforeunveil');
|
|
pager.dispatch('lazybeforeunveil');
|
|
assert.deepEqual(pageRequests(runtime.requests), [1]);
|
|
const url = new URL(runtime.requests[0]);
|
|
assert.equal(url.pathname, '/wp-json/aksrt/v1/widgets/posts/items');
|
|
assert.equal(url.searchParams.get('instance'), 'aksrt_widget_posts-2');
|
|
transport.pending[0].resolve({ html: '<posts class="posts-item list ajax-item" data-post-id="first">First article</posts>', page: 1, paginate_html: '' });
|
|
await tick();
|
|
assert.ok(pager.querySelector('[data-post-id="first"]'));
|
|
assert.equal(pager.dataset.loaded, '1');
|
|
assert.equal(pager.getAttribute('aria-busy'), 'false');
|
|
assert.equal(pager.querySelector('.post_ajax_loader').classList.contains('is-loading'), false);
|
|
pager.dispatch('lazybeforeunveil');
|
|
assert.equal(runtime.requests.length, 1, 'a completed first page is never fetched again by lazySizes');
|
|
});
|
|
|
|
test('article widget loads when lazySizes unveiled it before the widget client initialized', async () => {
|
|
const runtime = createRuntime({ posts: true, unveiled: true });
|
|
assert.deepEqual(pageRequests(runtime.requests), [1]);
|
|
await tick();
|
|
const pager = runtime.widget.querySelector('[data-widget-items]');
|
|
assert.equal(pager.dataset.loaded, '1');
|
|
assert.ok(pager.querySelector('[data-post-id]'));
|
|
});
|
|
|
|
test('article widget failure exposes a retry that replaces the initial skeleton', async () => {
|
|
const transport = pendingFetch();
|
|
const runtime = createRuntime({ posts: true, fetch: transport.fetch });
|
|
const pager = runtime.widget.querySelector('[data-widget-items]');
|
|
pager.dispatch('lazybeforeunveil');
|
|
transport.pending[0].reject(new Error('offline'));
|
|
await tick();
|
|
assert.equal(pager.getAttribute('aria-busy'), 'false');
|
|
assert.equal(pager.querySelector('.post_ajax_loader').classList.contains('is-loading'), false);
|
|
pager.querySelector('[data-widget-retry]').click();
|
|
assert.deepEqual(pageRequests(runtime.requests), [1, 1]);
|
|
transport.pending[1].resolve({ html: '<posts class="posts-item card ajax-item" data-post-id="retried">Retried</posts>', page: 1, paginate_html: '' });
|
|
await tick();
|
|
assert.ok(pager.querySelector('[data-post-id="retried"]'));
|
|
assert.equal(pager.querySelector('[data-widget-retry]'), null);
|
|
});
|
|
|
|
test('article REST addresses preserve the route for plain WordPress permalinks', async () => {
|
|
const runtime = createRuntime({ posts: true, unveiled: true, restUrl: 'https://example.test/blog/?rest_route=/aksrt/v1/widgets/' });
|
|
const url = new URL(runtime.requests[0]);
|
|
assert.equal(url.pathname, '/blog/');
|
|
assert.equal(url.searchParams.get('rest_route'), '/aksrt/v1/widgets/posts/items');
|
|
assert.equal(url.searchParams.get('instance'), 'aksrt_widget_posts-2');
|
|
assert.equal(url.searchParams.get('page'), '1');
|
|
await tick();
|
|
assert.equal(runtime.widget.querySelector('[data-widget-items]').dataset.loaded, '1');
|
|
});
|
|
|
|
test('a pager inserted as the mutation root still receives its lazy listener', async () => {
|
|
const runtime = createRuntime({ observe: true });
|
|
const pager = postsMarkup(runtime.document).root.querySelector('[data-widget-items]');
|
|
pager.nodeType = 1;
|
|
runtime.document.body.appendChild(pager);
|
|
runtime.notifyMutation([{ addedNodes: [pager] }]);
|
|
pager.dispatch('lazybeforeunveil');
|
|
await tick();
|
|
assert.deepEqual(pageRequests(runtime.requests), [1]);
|
|
assert.equal(pager.dataset.loaded, '1');
|
|
});
|