fix(widgets): restore article list rendering and reference styles

This commit is contained in:
kate522 committed 2026-10-30 12:47:07 +08:00
1 parent 4ea00f1322
commit fc04bf65e6
18 files changed
+345 -97

No files matched your search

@@ -32,8 +32,8 @@ test('term aggregation renders the real SVG use element, total count, and config
const firstQuery = fixture.aggregation_queries[0];
assert.equal(firstQuery.posts_per_page, 2);
assert.equal(firstQuery.no_found_rows, false);
assert.deepEqual(firstQuery.category__in, [7]);
assert.equal(fixture.aggregation_queries[1].category__in[0], 8);
assert.equal(firstQuery.cat, '7');
assert.equal(fixture.aggregation_queries[1].cat, '8');
});
test('category filters keep dropdown and inline terms in separate rendered groups on the ajax-option pipeline', () => {
@@ -19,7 +19,12 @@ class WP_Query {
public function __construct(public array $query_args = []) {
$GLOBALS['discovery_queries'][] = $query_args;
$cats = array_map('intval', (array) ($query_args['category__in'] ?? []));
$rows = array_values(array_filter($GLOBALS['discovery_posts'], static fn(array $post): bool => !$cats || in_array($post['cat'], $cats, true)));
$excluded = [];
foreach (array_filter(explode(',', (string) ($query_args['cat'] ?? ''))) as $cat) {
if ((int) $cat > 0) $cats[] = (int) $cat;
if ((int) $cat < 0) $excluded[] = abs((int) $cat);
}
$rows = array_values(array_filter($GLOBALS['discovery_posts'], static fn(array $post): bool => (!$cats || in_array($post['cat'], $cats, true)) && !in_array($post['cat'], $excluded, true)));
$this->found_posts = count($rows);
$per_page = max(1, (int) ($query_args['posts_per_page'] ?? 10));
$this->max_num_pages = (int) ceil($this->found_posts / $per_page);
@@ -0,0 +1,35 @@
'use strict';
const assert = require('node:assert/strict');
const { execFileSync } = require('node:child_process');
const path = require('node:path');
const test = require('node:test');
const cases = JSON.parse(execFileSync(process.env.PHP_BINARY || 'php', [
path.join(__dirname, 'tab-posts-render-harness.php'),
], { encoding: 'utf8' })).cases;
for (const style of ['list', 'card', 'mini']) {
test(`article ${style} widget renders its saved layout directly`, () => {
const html = cases[`posts_${style}_detail`];
assert.match(html, new RegExp(`class="widget-main-post mb20 style-${style}"`));
assert.match(html, /data-widget="posts" data-instance="aksrt_widget_posts-3"/);
assert.match(html, /data-loaded="1" aria-live="polite" aria-busy="false"/);
assert.equal((html.match(/<h2 class="item-heading\b/g) || []).length, 4);
assert.match(html, /data-widget-page="2"/);
assert.match(html, /class="post_ajax_loader"/);
assert.doesNotMatch(html, /post_ajax_loader is-loading|class="posts-row|ajax-next/);
if (style === 'mini') assert.match(html, /class="zib-widget posts-mini-lists"/);
});
test(`article ${style} widget reserves visible skeletons for lazy loading`, () => {
const html = cases[`posts_${style}_ajax`];
assert.match(html, new RegExp(`class="widget-main-post mb20 style-${style}"`));
assert.match(html, /class="widget-ajaxpager lazyload" data-widget-lazy/);
assert.match(html, /data-widget="posts" data-instance="aksrt_widget_posts-3"/);
assert.match(html, /aria-live="polite" aria-busy="true"/);
assert.match(html, /class="post_ajax_loader is-loading" role="status"/);
assert.match(html, new RegExp(`data-placeholder="${style}:4"`));
assert.doesNotMatch(html, /class="item-heading|ias-btn|data-widget-ias|post_ajax_trigger|lazyload-action=/);
});
}
@@ -0,0 +1,37 @@
'use strict';
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const test = require('node:test');
const root = path.resolve(__dirname, '..', '..');
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
test('article list uses the reference widget container and keeps widget behavior classes', () => {
const renderer = read('src/WidgetSystem/FrontendRenderer.php');
const catalog = read('src/WidgetSystem/WidgetCatalog.php');
assert.match(renderer, /\['user_card', 'author_card', 'tab_posts', 'posts'\]/);
assert.match(renderer, /\$classes = \['zib-widget-wrap', 'aksrt-' \. str_replace/);
assert.match(renderer, /echo '<div class="widget-container"><div>'/);
assert.match(renderer, /WidgetVisibility::device_class\(\$instance\)/);
assert.match(renderer, /!empty\(\$instance\['sidebar_affix'\]\)/);
assert.match(renderer, /data-affix="true"/);
assert.match(catalog, /in_array\(\$slug, \['buttons', 'notice', 'posts'\], true\)/);
});
test('article list pagination shares the reference widget rules and obsolete trigger CSS is removed', () => {
const css = read('assets/widgets/frontend.css');
assert.match(css, /\.widget-tab-post \.pagenav, \.widget-main-post \.pagenav/);
assert.match(css, /\.sidebar :is\(\.widget-tab-post, \.widget-main-post\) \.pagenav/);
assert.match(css, /:is\(\.widget-tab-post, \.widget-main-post\) \.pagenav a\.page-numbers/);
assert.doesNotMatch(css, /\.post_ajax_trigger/);
for (const token of [
'.widget-tab-post .item-heading, .widget-main-post .item-heading',
'.widget-tab-post .item-heading.font-bold, .widget-main-post .item-heading.font-bold',
'.widget-tab-post .posts-mini, .widget-main-post .posts-mini',
'.widget-tab-post .posts-mini-con, .widget-main-post .posts-mini-con',
'.widget-tab-post .item-meta .meta-pay, .widget-main-post .item-meta .meta-pay',
'.aksrt-posts-wrap :where(a, button, input, select, textarea, [tabindex]):focus-visible',
]) assert.ok(css.includes(token), `missing shared widget rule: ${token}`);
});
+20 -3
View File
@@ -18,6 +18,12 @@ use AKSRT\WidgetSystem\Renderers\ContentRenderer;
use AKSRT\WidgetSystem\Renderers\GeneralRenderer;
use AKSRT\WidgetSystem\Renderers\SliderRenderer;
if (!function_exists('current_datetime')) {
function current_datetime(): DateTimeImmutable {
return new DateTimeImmutable('2026-06-15 12:30:00', new DateTimeZone('Asia/Shanghai'));
}
}
final class WidgetSystemTest {
private int $assertions = 0;
private array $failures = [];
@@ -105,15 +111,24 @@ final class WidgetSystemTest {
'count' => 6, 'cat' => '1, -2,3 bad',
'orderby' => 'views', 'order' => 'DESC', 'limit_day' => 7, 'paginate' => '',
], 3);
$this->same([1, 3], $reference['category__in'], 'reference cat includes positive IDs');
$this->same([2], $reference['category__not_in'], 'reference cat excludes negative IDs');
$this->same('1,-2,3', $reference['cat'], 'reference cat preserves cleaned signed IDs for WP_Query descendant semantics');
$this->false(isset($reference['category__in']) || isset($reference['category__not_in']), 'reference cat does not narrow matches to direct categories');
$this->false(isset($reference['tax_query']), 'topic filters are removed with the topics feature');
$this->same('aksrt_post_views', $reference['meta_key'], 'reference view ordering uses numeric view meta');
$this->false(isset($reference['meta_query']), 'modern reference queries do not apply commerce filters by default');
$this->same(1, $reference['paged'], 'no-pagination queries stay on first page');
$this->same('2026-06-08 12:30:00', $reference['date_query'][0]['after'], 'limit_day subtracts days in the WordPress site timezone');
$this->same('2026-06-15 12:30:00', $reference['date_query'][0]['before'], 'limit_day uses local WordPress time without a second offset conversion');
$generic_categories = PostQuery::arguments(['category_ids' => '2,5', 'category_id' => 9], 1);
$this->same([9], $generic_categories['category__in'], 'generic category_id continues to override category_ids');
$this->false(isset($generic_categories['cat']), 'generic categories do not switch to modern cat syntax');
$paywalled = PostQuery::arguments(['cat' => '1', 'zibpay_type' => ['1', '6'], 'paginate' => ''], 1, ['cat' => '1']);
$this->contains('reading', json_encode($paywalled['meta_query'] ?? []), 'tab commerce filters map to pay meta');
$this->contains('video', json_encode($paywalled['meta_query'] ?? []), 'tab commerce filters cover every selected type');
$top_level_paywall = PostQuery::arguments(['cat' => '1', 'zibpay_type' => ['2'], 'paginate' => 'ajax'], 1);
$this->contains('download', json_encode($top_level_paywall['meta_query'] ?? []), 'top-level reference commerce filters map to pay meta');
$generic_paywall = PostQuery::arguments(['category_ids' => '1', 'pay_type' => 'gallery'], 1);
$this->contains('gallery', json_encode($generic_paywall['meta_query'] ?? []), 'generic widget pay_type behavior remains available');
$invalid = PostQuery::arguments(['cat' => '', 'orderby' => 'drop table'], 1, ['cat' => '']);
$this->false(isset($invalid['meta_query']), 'modern reference fields never add commerce filters');
$this->same('date', $invalid['orderby'], 'unknown orderby falls back');
@@ -298,7 +313,7 @@ final class WidgetSystemTest {
$error = $controller->get_items(new WP_REST_Request(['widget' => 'posts', 'instance' => 'aksrt_widget_posts-1', 'page' => 1]));
$this->error('aksrt_widget_instance_not_registered', $error, 'REST requires an assigned instance');
$GLOBALS['aksrt_test_state']['sidebars'] = ['home_top_content' => ['aksrt_widget_posts-1']];
update_option('widget_aksrt_widget_posts', [1 => ['title' => 'Saved only', 'count' => 2, 'style' => 'list'], '_multiwidget' => 1]);
update_option('widget_aksrt_widget_posts', [1 => ['title' => 'Saved only', 'count' => 2, 'style' => 'list', 'paginate' => 'ajax'], '_multiwidget' => 1]);
$response = $controller->get_items(new WP_REST_Request(['widget' => 'posts', 'instance' => 'aksrt_widget_posts-1', 'page' => 2, 'segment' => '']));
$this->true($response instanceof WP_REST_Response, 'registered instance returns REST response');
$data = $response->get_data();
@@ -306,6 +321,8 @@ final class WidgetSystemTest {
$this->same(2, $data['page'], 'REST page reaches saved-query pipeline');
$this->contains('暂无内容', $data['html'], 'REST returns deterministic empty state');
$this->same(4, end($GLOBALS['aksrt_test_state']['query_args'])['posts_per_page'], 'REST reloads saved normalized configuration');
$response = $controller->get_items(new WP_REST_Request(['widget' => 'posts', 'instance' => 'aksrt_widget_posts-1', 'page' => 1, 'rest_route' => '/aksrt/v1/widgets/posts/items']));
$this->true($response instanceof WP_REST_Response, 'plain permalink transport parameter is accepted');
$error = $controller->get_items(new WP_REST_Request(['widget' => 'posts', 'instance' => 'aksrt_widget_hot_posts-1', 'page' => 1]));
$this->error('aksrt_widget_invalid_instance', $error, 'REST binds instance ID to requested widget');
}
@@ -33,15 +33,15 @@ test('un-normalized missing and invalid style use one shared mini resolution in
}
});
test('mini options render thumbnail, rank, author and meta on both pages with continuous numbering', () => {
test('mini options render thumbnail, rank, author and meta; rank is limited to the first page', () => {
const current = render().mini_all_options;
assert.equal((current.first.match(/class="posts-mini ajax-item"/g) || []).length, 4);
assert.match(current.first, /class="mr10"><div class="item-thumbnail"/);
assert.match(current.first, /badg badg-sm mr3 c-red">1<\/span>/);
assert.match(current.first, /class="meta-author flex ac"/);
assert.match(current.first, /class="item-meta muted-2-color flex jsb ac/);
assert.match(current.second, /badg badg-sm mr3 c-green">5<\/span>/);
assert.match(current.second, /真实文章 36/);
assert.doesNotMatch(current.second, /class="badg badg-sm/);
assert.match(current.second, /真实文章 32/);
assert.doesNotMatch(current.first + current.second, /class="posts-row|class="posts-item list|class="posts-item card/);
});
@@ -152,12 +152,14 @@ test('multi-column frontend output uses the reference widget wrapper and carries
assert.doesNotMatch(wrapper, /aksrt-widget__inner container|<aside>|<\/aside>/);
const frontend = read('src/WidgetSystem/FrontendRenderer.php');
const routeStart = frontend.indexOf("if (in_array($definition->slug(), ['user_card', 'author_card', 'tab_posts']");
const routeStart = frontend.indexOf("if (in_array($definition->slug(), ['user_card', 'author_card', 'tab_posts', 'posts']");
const contentRouteStart = frontend.indexOf('if (in_array($definition->slug()', routeStart + 1);
const referenceRoute = frontend.slice(routeStart, contentRouteStart);
const contentRoute = frontend.slice(contentRouteStart, frontend.indexOf('private static function render_reference_widget'));
assert.match(referenceRoute, /'user_card', 'author_card', 'tab_posts'/);
assert.doesNotMatch(contentRoute, /'tab_posts'/);
assert.match(referenceRoute, /'user_card', 'author_card', 'tab_posts', 'posts'/);
assert.doesNotMatch(contentRoute, /'tab_posts'|'posts'/);
assert.match(frontend, /\$affix_attr = !empty\(\$instance\['sidebar_affix'\]\) \? ' data-affix="true"'/);
assert.match(frontend, /\. \$affix_attr \. \(\$style/);
});
test('widget list and card CSS is included in the public page bundle at desktop and mobile breakpoints', () => {
@@ -31,6 +31,10 @@ class WP_Query {
$rows = array_values(array_filter($GLOBALS['tab_fixture_posts'], static function (array $post) use ($args): bool {
$include = array_map('intval', (array) ($args['category__in'] ?? []));
$exclude = array_map('intval', (array) ($args['category__not_in'] ?? []));
foreach (array_filter(explode(',', (string) ($args['cat'] ?? ''))) as $cat) {
if ((int) $cat > 0) $include[] = (int) $cat;
if ((int) $cat < 0) $exclude[] = abs((int) $cat);
}
return (!$include || in_array($post['cat'], $include, true)) && !in_array($post['cat'], $exclude, true);
}));
$orderby = (string) ($args['orderby'] ?? 'date');
@@ -239,7 +243,7 @@ namespace {
'avatar_calls' => array_slice($GLOBALS['tab_fixture_avatar_calls'], $avatar_start),
];
}
$mini_instance = ['style' => 'mini', 'mini_opt' => ['show_thumb', 'show_number', 'show_meta'], 'count' => 4, 'paginate' => '', 'tabs' => $tabs];
$mini_instance = ['style' => 'mini', 'mini_opt' => ['show_thumb', 'show_number', 'show_meta'], 'count' => 4, 'paginate' => 'number', 'tabs' => $tabs];
$mini_instance = \AKSRT\WidgetSystem\FieldSchema::normalize(\AKSRT\WidgetSystem\WidgetSchemas::for('tab_posts'), $mini_instance);
$avatar_start = count($GLOBALS['tab_fixture_avatar_calls']);
$mini_first = \AKSRT\WidgetSystem\PostRenderer::payload($mini_instance, 'tab_posts', 1, '0')['html'];
@@ -249,6 +253,14 @@ namespace {
'second' => $mini_second,
'avatar_calls' => array_slice($GLOBALS['tab_fixture_avatar_calls'], $avatar_start),
];
$cases['no_pagination_page'] = \AKSRT\WidgetSystem\PostRenderer::payload(array_merge($mini_instance, ['paginate' => '']), 'tab_posts', 2, '0');
foreach (['list', 'card', 'mini'] as $posts_style) {
$posts_definition = \AKSRT\WidgetSystem\WidgetRegistry::get('posts');
foreach (['detail', 'ajax'] as $load_mode) {
$posts_instance = $posts_definition->normalize(['style' => $posts_style, 'load_mode' => $load_mode, 'count' => 4, 'paginate' => 'number', 'title' => '文章列表']);
$cases['posts_' . $posts_style . '_' . $load_mode] = \AKSRT\WidgetSystem\Renderers\ContentRenderer::render('posts', $posts_instance, ['widget_id' => 'aksrt_widget_posts-3']);
}
}
$GLOBALS['tab_fixture_global_style']['post_list_author'] = false;
$mini_time_instance = ['style' => 'mini', 'mini_opt' => ['show_meta'], 'count' => 4, 'paginate' => '', 'tabs' => $tabs];
$mini_time_instance = \AKSRT\WidgetSystem\FieldSchema::normalize(\AKSRT\WidgetSystem\WidgetSchemas::for('tab_posts'), $mini_time_instance);
+102 -5
View File
@@ -332,9 +332,25 @@ function addJumpControl(document, pager, { max = 20, value = '4' } = {}) {
return { input, button };
}
function createRuntime({ fetch, pages } = {}) {
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 } = tabMarkup(document);
const { root: widget, nav, tabOne, tabTwo } = posts ? postsMarkup(document, unveiled) : tabMarkup(document);
document.body.appendChild(widget);
const autoFun = [];
function jQuery() {
@@ -344,7 +360,7 @@ function createRuntime({ fetch, pages } = {}) {
};
}
const requests = [];
const config = { restUrl: '/wp-json/aksrt/v1', i18n: { error: 'error', retry: 'retry' } };
const config = { restUrl, i18n: { error: 'error', retry: 'retry' } };
const fetchStub = (url) => {
requests.push(url);
if (fetch) return fetch(url);
@@ -357,11 +373,18 @@ function createRuntime({ fetch, pages } = {}) {
};
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,
@@ -373,9 +396,10 @@ function createRuntime({ fetch, pages } = {}) {
Object,
Math,
encodeURIComponent,
MutationObserver: undefined,
URL,
MutationObserver: Observer,
});
return { document, widget, nav, tabOne, tabTwo, requests, autoFun };
return { document, widget, nav, tabOne, tabTwo, requests, autoFun, notifyMutation };
}
const tick = () => new Promise((resolve) => setImmediate(resolve));
@@ -677,3 +701,76 @@ test('jump input clamps pages to max and rejects invalid or current pages withou
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');
});
@@ -20,7 +20,7 @@ function declarations(selector) {
return result;
}
test('tab post titles restore reference weight after public typography while preserving the bold option', () => {
test('tab and single post widget titles restore reference weight while preserving the bold option', () => {
// public.css sets h2 to 700. The widget must override that selector;
// the explicit global heading-bold option must still win inside it.
assert.match(read('assets/css/core/typography.css'), /font-weight:\s*700/);
@@ -28,12 +28,19 @@ test('tab post titles restore reference weight after public typography while pre
assert.equal(heading['font-weight'], '500');
assert.equal(heading.color, 'inherit');
assert.equal(declarations('.widget-tab-post .item-heading.font-bold')['font-weight'], '700');
assert.equal(declarations('.widget-main-post .item-heading')['font-weight'], '500');
assert.equal(declarations('.widget-main-post .item-heading.font-bold')['font-weight'], '700');
const shell = declarations('.aksrt-tab-posts-wrap');
assert.equal(shell['font-size'], '14px');
assert.equal(shell['line-height'], '1.42857143');
assert.equal(shell.color, 'var(--main-color,#4e5358)');
assert.equal(declarations('.widget-tab-post .posts-mini')['vertical-align'], 'baseline');
assert.equal(declarations('.widget-tab-post .posts-mini-con').flex, 'auto');
assert.equal(declarations('.widget-main-post .posts-mini')['vertical-align'], 'baseline');
assert.equal(declarations('.widget-main-post .posts-mini-con').flex, 'auto');
assert.equal(declarations('.widget-tab-post .item-meta .meta-pay').cursor, 'auto');
assert.equal(declarations('.widget-main-post .item-meta .meta-pay').cursor, 'auto');
assert.match(css, /\.aksrt-posts-wrap :where\(a, button, input, select, textarea, \[tabindex\]\):focus-visible/);
});
test('widget pagination restores compact reference metrics over public pagination overrides', () => {
@@ -52,6 +59,10 @@ test('widget pagination restores compact reference metrics over public paginatio
assert.equal(nestedLabel['min-width'], '0');
assert.equal(nestedLabel.padding, '0');
const hover = declarations('.widget-tab-post .pagenav a.page-numbers:hover');
assert.equal(declarations('.widget-main-post .pagenav').display, 'block');
assert.equal(declarations('.widget-main-post .pagenav .page-numbers').padding, '6px 9px 5px');
assert.equal(declarations('.widget-main-post .pagenav a.page-numbers:hover').opacity, '.7');
assert.match(read('assets/css/main.css'), /\.pagenav \.current\s*\{[^}]*background-color:\s*var\(--focus-color\)\s*!important;[^}]*color:\s*#fff\s*!important;/s);
assert.equal(hover.background, 'var(--main-border-color)');
assert.equal(hover.opacity, '.7');
});
@@ -61,13 +72,15 @@ test('jump control keeps the reference inline layout and responsive numeric link
assert.equal(jump.width, 'auto', 'public mobile CSS must not stretch the jump control to a full row');
assert.equal(jump.display, 'inline-flex');
const input = declarations('.widget-tab-post .pagenav .pag-jump .jump-input');
assert.equal(declarations('.widget-main-post .pagenav .pag-jump').display, 'inline-flex');
assert.equal(declarations('.widget-main-post .pagenav .pag-jump .jump-input').position, 'absolute');
assert.equal(input.position, 'absolute');
assert.equal(input.width, '100%');
assert.equal(input.opacity, '0');
const mobile = css.slice(css.indexOf('@media (max-width:640px)'));
assert.match(mobile, /\.widget-tab-post \.pagenav a\.page-numbers\s*\{\s*display:none/);
assert.match(mobile, /\.widget-tab-post \.pagenav a\.prev\s*\{\s*display:inline-block/);
assert.match(mobile, /\.widget-tab-post \.pagenav \.page-numbers\s*\{\s*min-width:28px;\s*margin:2px;\s*font-size:12px/);
assert.match(mobile, /:is\(\.widget-tab-post, \.widget-main-post\) \.pagenav a\.page-numbers\s*\{\s*display:none/);
assert.match(mobile, /:is\(\.widget-tab-post, \.widget-main-post\) \.pagenav a\.prev\s*\{\s*display:inline-block/);
assert.match(mobile, /:is\(\.widget-tab-post, \.widget-main-post\) \.pagenav \.page-numbers\s*\{\s*min-width:28px;\s*margin:2px;\s*font-size:12px/);
});
test('modern tabs have one interaction owner and no retired tab selector or unused widget-style field', () => {
@@ -20,8 +20,8 @@ test('modern widget schemas expose Zibll field names and defaults', () => {
test('tab and slider renderers retain reference DOM contracts', () => {
const content = read('src/WidgetSystem/Renderers/ContentRenderer.php');
const slider = read('src/WidgetSystem/Renderers/SliderRenderer.php');
for (const token of ['widget-tab-post', 'posts-mini-lists', 'index-tab', 'tab-nav-theme', 'tab-pane fade', 'ias-btn', 'post_ajax_loader', 'widget-ajaxpager']) assert.match(content, new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
assert.doesNotMatch(content, /post_ajax_trigger/);
for (const token of ['widget-tab-post', 'posts-mini-lists', 'index-tab', 'tab-nav-theme', 'tab-pane fade', 'data-widget-lazy', 'post_ajax_loader', 'widget-ajaxpager']) assert.match(content, new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
assert.doesNotMatch(content, /post_ajax_trigger|ias-btn|data-widget-ias/);
for (const token of ['relative zib-slider', 'new-swiper slide-widget', 'swiper-wrapper', 'swiper-slide', 'slide-layer', 'data-swiper-parallax', 'swiper-button-prev', 'swiper-pagination', 'widget-slider-graphic', 'slider-col', 'cover-col', 'widget-graphic-cover']) assert.match(slider, new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')));
});