The theme was renamed Zibll/子比 → AKSRT → Jio, but upstream branding survived the sweeps in CSS classes, the SVG sprite prefix, a persisted widget field ID, JS events, admin Vue class names, test infrastructure and docblocks. Renames: - zib-widget / zib-widget-wrap / zib-slider → jio-widget-box / jio-widget-box-wrap / jio-slider-box (jio-widget is taken by the modern widget shell, jio-box-* by the card primitives) - zib-partners-scroll + zps-* → jio-partners-scroll + jps-* - zib-combo-info-card + zci-* → jio-combo-info-card + jci-* - zibsvg- → jiosvg-, including the exported picker helpers and the fa-zibsvg- family-prefix guard - zibpay_type → pay_type; the dual-read fallback in PostQuery collapses to a single key - [zibajax="submit"] dropped (no PHP emitter); zib_ajax.success → jio_ajax.success - aks-* admin Vue classes → jio-* - zib_palette() → palette() User-facing vendor traces are gone: the floating "more" button default no longer ships a zibll.com link or 子比主题 marketing copy, the notice widget ships no preset promo entries, and the four 官网教程 links are removed from the post-list admin schema. Test infrastructure renamed to neutral names (docker-compose.reference.yml, Caddyfile, article-baseline.json, modern-widgets-contract.test.cjs) with their DB/volume/env/slug identifiers following. Kept intentionally, documented in tests/fixtures/README.md: the golden fixture reference fingerprint (a SHA-256 record of an external directory, read by validate-widget-golden / generate-widget-golden-fixture / audit-widget-system), the doesNotMatch guards that prove the old tokens are gone, and the reference theme's own meta key in the visual fixtures. No backward-compat or data migration: saved widget configs holding old icon keys or the old field ID silently lose them, per the project's rewrite-over-compatibility rule. Verified: 132/132 node contract tests, php tests/widget-system/run.php (146 assertions), audit-admin-contracts, audit-psr4-paths and validate-widget-golden all green; tracked bundles regenerated and in sync. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
177 lines
9.9 KiB
JavaScript
177 lines
9.9 KiB
JavaScript
const test = require('node:test');
|
|
const assert = require('node:assert/strict');
|
|
const { execFileSync } = require('node:child_process');
|
|
const fs = require('node:fs');
|
|
const path = require('node:path');
|
|
|
|
const root = path.resolve(__dirname, '..', '..');
|
|
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8');
|
|
|
|
function render() {
|
|
const output = execFileSync('php', [path.join(__dirname, 'tab-posts-render-harness.php')], { encoding: 'utf8' });
|
|
return JSON.parse(output).cases;
|
|
}
|
|
|
|
test('missing and invalid tab style use the reference mini layout and ignore global post-card settings', () => {
|
|
const cases = render();
|
|
for (const name of ['default', 'invalid']) {
|
|
const current = cases[name];
|
|
assert.equal(current.instance_style, 'mini');
|
|
assert.match(current.tab_markup, /widget-tab-post style-mini posts-mini-lists jio-widget-box/);
|
|
assert.match(current.first, /class="posts-mini ajax-item"/);
|
|
assert.doesNotMatch(current.first, /class="mr10"|class="item-meta|class="badg badg-sm/);
|
|
assert.doesNotMatch(current.first, /class="posts-item list|class="posts-item card|class="no_margin/);
|
|
}
|
|
});
|
|
|
|
test('un-normalized missing and invalid style use one shared mini resolution in both renderers', () => {
|
|
const cases = render();
|
|
for (const name of ['raw_default', 'raw_invalid']) {
|
|
assert.match(cases[name].tab_markup, /widget-tab-post style-mini posts-mini-lists jio-widget-box/);
|
|
assert.match(cases[name].first, /class="posts-mini ajax-item"/);
|
|
assert.doesNotMatch(cases[name].first, /class="posts-item list|class="posts-item card/);
|
|
}
|
|
});
|
|
|
|
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.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/);
|
|
});
|
|
|
|
test('mini, list and card meta use the linked avatar box with its badge image contract', () => {
|
|
const cases = render();
|
|
const avatarBox = /<a href="https:\/\/example\.test\/author\/5"><span class="avatar-mini"><img class="avatar"[^>]*><img class="avatar-badge"[^>]*><\/span><\/a>/;
|
|
for (const name of ['list', 'card', 'mini_all_options']) {
|
|
const current = cases[name];
|
|
assert.match(current.first, avatarBox, 'the shared linked avatar wrapper and badge stay in the article meta');
|
|
assert.doesNotMatch(current.first, /<img class="avatar-mini"/, 'the renderer must not fall back to a plain avatar image');
|
|
assert.ok(current.avatar_calls.length > 0, 'the shared avatar renderer receives each visible author');
|
|
for (const call of current.avatar_calls) {
|
|
assert.deepEqual(call, { user_id: 5, class: 'avatar-mini', link: true, vip: true });
|
|
}
|
|
}
|
|
});
|
|
|
|
test('mini publication time follows list_orderby even when the generic list uses modified time', () => {
|
|
const cases = render();
|
|
assert.match(cases.mini_time_source.html, /title="2026-01-01 00:00:00" class="icon-circle mln3">发布前/);
|
|
assert.match(cases.list.first, /title="2026-02-01 00:00:00" class="icon-circle">修改前/);
|
|
});
|
|
|
|
test('list tabs use the reference list item structure without inheriting archive context', () => {
|
|
const current = render().list;
|
|
assert.match(current.tab_markup, /widget-tab-post style-list index-tab/);
|
|
assert.match(current.first, /<posts class="posts-item list ajax-item style2 flex xx"/);
|
|
assert.match(current.first, /class="post-graphic"/);
|
|
assert.match(current.first, /class="item-excerpt muted-color text-ellipsis-2 mb6"/);
|
|
assert.match(current.first, /class="meta-author flex ac"/);
|
|
assert.doesNotMatch(current.first, /posts-row|no-thumb|mult-thumb|no_margin/);
|
|
});
|
|
|
|
test('card tabs render standalone cards without the archive list excerpt', () => {
|
|
const current = render().card;
|
|
assert.match(current.tab_markup, /widget-tab-post style-card index-tab/);
|
|
assert.match(current.first, /<posts class="posts-item card ajax-item style3"/);
|
|
assert.match(current.first, /class="item-thumbnail/);
|
|
assert.match(current.first, /class="item-body"/);
|
|
assert.doesNotMatch(current.first, /posts-row|item-excerpt|mult-thumb|no-thumb/);
|
|
});
|
|
|
|
test('blank tab titles are skipped while active and lazy panes retain matching segment identities', () => {
|
|
const markup = render().mini.tab_markup;
|
|
assert.equal((markup.match(/role="tab"/g) || []).length, 2);
|
|
assert.match(markup, /aria-selected="true" tabindex="0" data-segment="0"/);
|
|
assert.match(markup, /aria-selected="false" tabindex="-1" data-segment="2"/);
|
|
assert.doesNotMatch(markup, /data-segment="1"/);
|
|
assert.match(markup, /class="tab-pane fade active in"/);
|
|
assert.match(markup, /data-widget-items data-widget="tab_posts"[^>]*data-segment="2"/);
|
|
});
|
|
|
|
test('pagination follows the reference boundaries, ellipsis, jump control, and final-page rules', () => {
|
|
const cases = render();
|
|
const first = cases.list.pagination;
|
|
const last = cases.list.last_pagination;
|
|
assert.match(first, /class="page-numbers current">1<\/span>/);
|
|
assert.match(first, /data-widget-page="2"/);
|
|
assert.match(first, /class="page-numbers dots">…<\/span>/);
|
|
assert.match(first, /data-widget-page="9"/);
|
|
assert.match(first, /max="9" current="1"[^>]*data-widget-jump/);
|
|
assert.doesNotMatch(first, /class="next-page page-numbers prev"/);
|
|
assert.match(last, /class="next-page page-numbers prev"[^>]*data-widget-page="8"/);
|
|
assert.match(last, /class="page-numbers current">9<\/span>/);
|
|
assert.doesNotMatch(last, /class="next-page page-numbers next"/);
|
|
assert.match(last, /max="9" current="9"[^>]*data-widget-jump/);
|
|
assert.match(cases.ajax_pagination.first, /data-next-page="2"/);
|
|
assert.equal(cases.ajax_pagination.last, '');
|
|
});
|
|
|
|
test('mobile pagination uses no middle neighbors and retains the jump control through the final page', () => {
|
|
const current = render().mobile_pagination;
|
|
const middleJump = current.middle.match(/<input\b[^>]*data-widget-jump[^>]*>/)?.[0] || '';
|
|
assert.match(middleJump, /type="number"/);
|
|
assert.match(middleJump, /inputmode="numeric"/);
|
|
assert.match(current.middle, /class="page-numbers current">5<\/span>/);
|
|
assert.match(current.middle, /data-widget-page="1"/);
|
|
assert.match(current.middle, /data-widget-page="9"/);
|
|
assert.doesNotMatch(current.middle, /class="next-page page-numbers"[^>]*data-widget-page="4"/);
|
|
assert.doesNotMatch(current.middle, /class="next-page page-numbers"[^>]*data-widget-page="6"/);
|
|
assert.equal((current.middle.match(/class="page-numbers dots"/g) || []).length, 2);
|
|
assert.match(current.last, /class="page-numbers current">9<\/span>/);
|
|
assert.match(current.last, /data-widget-page="8"/);
|
|
assert.doesNotMatch(current.last, /class="next-page page-numbers next"/);
|
|
const lastJump = current.last.match(/<input\b[^>]*data-widget-jump[^>]*>/)?.[0] || '';
|
|
assert.match(lastJump, /max="9"/);
|
|
assert.match(lastJump, /current="9"/);
|
|
assert.match(lastJump, /type="number"/);
|
|
});
|
|
|
|
test('list margin defaults inherit the site setting while an explicit widget false stays false', () => {
|
|
const current = render().context_margins;
|
|
assert.equal(current.implicit, true);
|
|
assert.equal(current.explicit_false, false);
|
|
});
|
|
|
|
test('multi-column frontend output uses the reference widget wrapper and carries layout variables', () => {
|
|
const wrapper = render().frontend_wrapper;
|
|
assert.match(wrapper, /class="jio-widget-box-wrap jio-tab-posts-wrap"/);
|
|
assert.match(wrapper, /data-jio-widget="tab_posts" data-widget-id="jio_widget_tab_posts-4"/);
|
|
assert.match(wrapper, /--jio-widget-space-top:8px/);
|
|
assert.match(wrapper, /--jio-widget-space-m-bottom:10px/);
|
|
assert.match(wrapper, /--jio-widget-bg-light:#ffffff/);
|
|
assert.match(wrapper, /--jio-widget-bg-dark:#111111/);
|
|
assert.match(wrapper, /widget-container/);
|
|
assert.match(wrapper, /class="widget-content"/);
|
|
assert.match(wrapper, /class="widget-tab-post style-mini/);
|
|
assert.doesNotMatch(wrapper, /jio-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', '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', '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', () => {
|
|
const css = fs.readFileSync(path.join(root, 'assets/css/frontend/post-card.css'), 'utf8');
|
|
const mainCss = fs.readFileSync(path.join(root, 'assets/css/main.css'), 'utf8');
|
|
const builder = fs.readFileSync(path.join(root, 'tools/build-public-assets.mjs'), 'utf8');
|
|
assert.match(builder, /'homepage\.css': \[[\s\S]*?'assets\/css\/frontend\/post-card\.css'/);
|
|
assert.match(css, /\.posts-mini-lists \.tab-nav-theme/);
|
|
assert.match(css, /\.widget-tab-post\.index-tab \.tab-content > \.tab-pane/);
|
|
assert.match(css, /\.widget-tab-post\.style-list \.posts-item/);
|
|
assert.match(css, /@media \(max-width: 767px\)[\s\S]*?\.widget-tab-post\.index-tab \.tab-content > \.tab-pane/);
|
|
assert.match(mainCss, /\.tab-nav-theme \{/);
|
|
assert.match(mainCss, /\.index-tab ul > li\.active \{/);
|
|
});
|