' . esc_html((string) ($item['title'] ?? '')) . '
' . esc_html((string) ($item['description'] ?? '')) . '
- ' . $features . '
diff --git a/.specs/plans/aksrt-widgets-rewrite.md b/.specs/plans/aksrt-widgets-rewrite.md index 77cc6a70..7c323a9a 100644 --- a/.specs/plans/aksrt-widgets-rewrite.md +++ b/.specs/plans/aksrt-widgets-rewrite.md @@ -34,7 +34,7 @@ ## Phase 4 — 自动化与视觉终验 -- [x] 完成 schema、查询、可见性、导入导出、REST 与支付适配测试。 +- [ ] 完成 schema、查询、可见性、导入导出、REST 与支付适配测试。 - [ ] 生成参考源码黄金 fixture,并完成 1440/1024/390、明暗模式截图矩阵。 - [ ] 验证交互、无障碍、控制台、网络和像素差异阈值。 -- [x] 复跑 WordPress 项目检测、全量 PHP 检查和管理端契约审计。 +- [ ] 复跑 WordPress 项目检测、全量 PHP 检查和管理端契约审计。 diff --git a/assets/widgets/admin-core.js b/assets/widgets/admin-core.js deleted file mode 100644 index fcb5b3b8..00000000 --- a/assets/widgets/admin-core.js +++ /dev/null @@ -1,44 +0,0 @@ -(function (root, factory) { - 'use strict'; - var api = factory(); - if (typeof module === 'object' && module.exports) module.exports = api; - else root.AKSRTWidgetAdminCore = api; -})(typeof globalThis !== 'undefined' ? globalThis : this, function () { - 'use strict'; - - function parseDependency(raw) { - if (Array.isArray(raw)) return raw; - try { - var parsed = JSON.parse(String(raw || '[]')); - return Array.isArray(parsed) ? parsed : []; - } catch (_error) { - return []; - } - } - - function dependencyVisible(rule, current) { - rule = parseDependency(rule); - if (rule.length < 3) return true; - var expected = String(rule[2]); - var actual = String(current == null ? '' : current); - return rule[1] === '==' ? actual === expected : actual !== expected; - } - - function replaceRepeaterIndexes(name, rowIndexes) { - var foundWidgetNumber = false; - var cursor = 0; - return String(name || '').replace(/\[(\d+|__index_[a-f0-9]+__)\]/g, function (match, index) { - if (!foundWidgetNumber && /^\d+$/.test(index)) { - foundWidgetNumber = true; - return match; - } - return cursor < rowIndexes.length ? '[' + Number(rowIndexes[cursor++]) + ']' : match; - }); - } - - return { - parseDependency: parseDependency, - dependencyVisible: dependencyVisible, - replaceRepeaterIndexes: replaceRepeaterIndexes - }; -}); diff --git a/assets/widgets/admin.js b/assets/widgets/admin.js index ef764a5e..f411b637 100644 --- a/assets/widgets/admin.js +++ b/assets/widgets/admin.js @@ -2,7 +2,6 @@ 'use strict'; const config = window.aksrtWidgetAdmin || {}; - const core = window.AKSRTWidgetAdminCore; function initForm(root) { const $root = $(root); @@ -69,7 +68,15 @@ $form.find('[name]').each(function () { const rowIndexes = $(this).parents('.aksrt-widget-repeater__row').get().reverse().map((row) => Number(row.dataset.rowIndex || 0)); if (!rowIndexes.length) return; - this.name = core.replaceRepeaterIndexes(this.name, rowIndexes); + let foundWidgetNumber = false; + let cursor = 0; + this.name = this.name.replace(/\[(\d+|__index_[a-f0-9]+__)\]/g, (match, index) => { + if (!foundWidgetNumber && /^\d+$/.test(index)) { + foundWidgetNumber = true; + return match; + } + return cursor < rowIndexes.length ? `[${rowIndexes[cursor++]}]` : match; + }); }); } @@ -82,10 +89,14 @@ function evaluateDependencies($form) { $form.find('[data-dependency]').each(function () { - const rule = core.parseDependency(this.dataset.dependency || '[]'); + let rule; + try { rule = JSON.parse(this.dataset.dependency || '[]'); } catch (_error) { return; } + if (!Array.isArray(rule) || rule.length < 3) return; const $row = $(this).closest('.aksrt-widget-repeater__row'); const current = fieldValue($row.length ? $row : $form, String(rule[0])); - this.hidden = !core.dependencyVisible(rule, current); + const expected = String(rule[2]); + const visible = rule[1] === '==' ? current === expected : current !== expected; + this.hidden = !visible; }); } diff --git a/assets/widgets/frontend.css b/assets/widgets/frontend.css index 919e49e3..62e24ff9 100644 --- a/assets/widgets/frontend.css +++ b/assets/widgets/frontend.css @@ -15,9 +15,6 @@ .aksrt-widget img, .aksrt-widget video { max-width: 100%; } .aksrt-widget a { color: inherit; text-decoration: none; } .aksrt-widget button, .aksrt-widget input { font: inherit; } -.aksrt-widget :where(a, button, input, select, textarea, [tabindex]):focus-visible { outline: 3px solid var(--focus-color, #ff5473) !important; outline-offset: 3px; } -.dark-theme .aksrt-widget :where(a, button, input, select, textarea, [tabindex]):focus-visible, -[data-theme="dark"] .aksrt-widget :where(a, button, input, select, textarea, [tabindex]):focus-visible { outline-color: #ffd166 !important; } .aksrt-widget__header { display: flex; align-items: flex-end; justify-content: space-between; gap: 18px; margin-bottom: 16px; } .aksrt-widget__header--big { display: block; text-align: center; margin-bottom: 30px; } .aksrt-widget__title { margin: 0; font-size: 20px; line-height: 1.35; font-weight: 700; } @@ -99,7 +96,6 @@ .aksrt-widget-comments { display: grid; gap: 15px; margin: 0; padding: 0; list-style: none; }.aksrt-widget-comments li { display: flex; gap: 11px; }.aksrt-widget-comments__avatar img { width: 42px; height: 42px; border-radius: 50%; }.aksrt-widget-comments p { margin: 3px 0; color: var(--muted-color,#8a8c98); font-size: 13px; line-height: 1.55; }.aksrt-widget-comments__source { font-size: 12px; opacity: .65; } .aksrt-widget-links { display: grid; grid-template-columns: repeat(4,minmax(0,1fr)); gap: 10px; }.aksrt-widget-links a { display: flex; flex-direction: column; gap: 4px; padding: 14px; border-radius: 9px; background: var(--main-bg,#fff); box-shadow: 0 6px 20px rgba(31,35,48,.06); }.aksrt-widget-links small { color: var(--muted-color,#8a8c98); }.aksrt-widget-links--simple { display: flex; flex-wrap: wrap; }.aksrt-widget-links--simple a { padding: 5px 9px; box-shadow: none; background: transparent; } .aksrt-widget-notice { display: flex; height: 48px; align-items: center; gap: 12px; padding: 0 16px; overflow: hidden; border-radius: 9px; background: var(--main-bg,#fff); box-shadow: 0 7px 22px rgba(31,35,48,.06); }.aksrt-widget-notice .swiper { height: 48px; flex: 1; }.aksrt-widget-notice .swiper-slide, .aksrt-widget-notice .swiper-slide a { display: flex; height: 48px; align-items: center; gap: 10px; }.aksrt-widget-notice .swiper-slide span { overflow: hidden; color: var(--muted-color,#8a8c98); text-overflow: ellipsis; white-space: nowrap; } -.aksrt-widget-swiper { position: relative; }.aksrt-widget-swiper__pause { position: absolute; z-index: 6; top: 10px; right: 10px; display: inline-flex; width: 34px; height: 34px; align-items: center; justify-content: center; border: 1px solid rgba(255,255,255,.45); border-radius: 50%; background: rgba(20,21,27,.72); color: #fff; cursor: pointer; }.aksrt-widget-notice .aksrt-widget-swiper__pause { top: 7px; right: 4px; width: 30px; height: 30px; } .aksrt-term-card { display: block; overflow: hidden; border-radius: var(--aksrt-widget-radius); background: var(--main-bg,#fff); box-shadow: 0 8px 28px rgba(31,35,48,.07); }.aksrt-term-card__media { display: block; aspect-ratio: 16/9; background: linear-gradient(135deg,#667eea,#764ba2); }.aksrt-term-card__media img { width: 100%; height: 100%; object-fit: cover; }.aksrt-term-card__body { display: flex; flex-direction: column; gap: 5px; padding: 14px; }.aksrt-term-card__body small { min-height: 38px; color: var(--muted-color,#8a8c98); }.aksrt-term-card__body em { font-size: 12px; color: var(--focus-color,#ff5473); font-style: normal; } .aksrt-term-aggregation { display: grid; grid-template-columns: repeat(2,minmax(0,1fr)); gap: 16px; }.aksrt-term-aggregation__card { padding: 16px; border-radius: var(--aksrt-widget-radius); background: var(--main-bg,#fff); box-shadow: 0 8px 28px rgba(31,35,48,.07); }.aksrt-term-aggregation header a { display: flex; justify-content: space-between; }.aksrt-term-aggregation header span { color: var(--muted-color,#8a8c98); font-size: 13px; }.aksrt-term-aggregation ul { display: grid; gap: 10px; margin: 14px 0 0; padding: 0; list-style: none; }.aksrt-term-aggregation li a { display: flex; align-items: center; gap: 10px; }.aksrt-term-aggregation li img { width: 55px; height: 42px; border-radius: 6px; object-fit: cover; } @@ -135,8 +131,5 @@ @media (prefers-reduced-motion: reduce) { .aksrt-widget *, .aksrt-widget *::before, .aksrt-widget *::after { scroll-behavior: auto !important; animation-duration: .01ms !important; animation-iteration-count: 1 !important; transition-duration: .01ms !important; } } -@media (forced-colors: active) { - .aksrt-widget :where(a, button, input, select, textarea, [tabindex]):focus-visible { outline-color: Highlight !important; } -} .aksrt-widget--animate { animation: aksrt-widget-fade .55s both; }.aksrt-widget--animate-slideup { animation-name: aksrt-widget-slideup; }.aksrt-widget--animate-slidedown { animation-name: aksrt-widget-slidedown; }.aksrt-widget--animate-slideright { animation-name: aksrt-widget-slideright; }.aksrt-widget--animate-slideleft { animation-name: aksrt-widget-slideleft; }.aksrt-widget--animate-zoomin { animation-name: aksrt-widget-zoomin; }.aksrt-widget--animate-zoomout { animation-name: aksrt-widget-zoomout; }.aksrt-widget--animate-repeat { animation-iteration-count: infinite; animation-duration: 2s; } @keyframes aksrt-widget-fade { from { opacity: 0; } }@keyframes aksrt-widget-slideup { from { opacity:0;transform:translateY(25px); } }@keyframes aksrt-widget-slidedown { from { opacity:0;transform:translateY(-25px); } }@keyframes aksrt-widget-slideright { from { opacity:0;transform:translateX(-25px); } }@keyframes aksrt-widget-slideleft { from { opacity:0;transform:translateX(25px); } }@keyframes aksrt-widget-zoomin { from { opacity:0;transform:scale(.9); } }@keyframes aksrt-widget-zoomout { from { opacity:0;transform:scale(1.1); } } diff --git a/assets/widgets/frontend.js b/assets/widgets/frontend.js index f84dcd4e..7e387aa3 100644 --- a/assets/widgets/frontend.js +++ b/assets/widgets/frontend.js @@ -11,59 +11,6 @@ return true; } - function moveTabFocus(event, root) { - var current = event.target.closest('[role="tab"]'); - if (!current) return; - var tabs = Array.prototype.slice.call(root.querySelectorAll('[role="tab"]')); - var index = tabs.indexOf(current); - var next = index; - if (event.key === 'ArrowRight') next = (index + 1) % tabs.length; - else if (event.key === 'ArrowLeft') next = (index - 1 + tabs.length) % tabs.length; - else if (event.key === 'Home') next = 0; - else if (event.key === 'End') next = tabs.length - 1; - else return; - event.preventDefault(); - tabs[next].focus(); - tabs[next].click(); - } - - function initSwiperPause(el, swiper) { - if (!swiper.autoplay) return; - var i18n = config.i18n || {}; - var manuallyPaused = false; - var button = document.createElement('button'); - button.type = 'button'; - button.className = 'aksrt-widget-swiper__pause'; - button.setAttribute('aria-pressed', 'false'); - - function pause() { - if (typeof swiper.autoplay.pause === 'function') swiper.autoplay.pause(); - else if (typeof swiper.autoplay.stop === 'function') swiper.autoplay.stop(); - } - function resume() { - if (typeof swiper.autoplay.resume === 'function') swiper.autoplay.resume(); - else if (typeof swiper.autoplay.start === 'function') swiper.autoplay.start(); - } - function updateButton() { - button.setAttribute('aria-pressed', manuallyPaused ? 'true' : 'false'); - button.setAttribute('aria-label', manuallyPaused ? (i18n.resume || '继续自动播放') : (i18n.pause || '暂停自动播放')); - button.textContent = manuallyPaused ? '▶' : 'Ⅱ'; - } - - button.addEventListener('click', function () { - manuallyPaused = !manuallyPaused; - if (manuallyPaused) pause(); - else resume(); - updateButton(); - }); - el.addEventListener('focusin', pause); - el.addEventListener('focusout', function (event) { - if (!manuallyPaused && !el.contains(event.relatedTarget)) resume(); - }); - updateButton(); - el.appendChild(button); - } - function initSwiper(el) { if (!once(el, 'Swiper') || typeof window.Swiper !== 'function') return; var kind = el.dataset.aksrtSwiper || 'cards'; @@ -90,13 +37,11 @@ options.effect = el.dataset.effect || 'slide'; options.parallax = !!el.querySelector('[data-swiper-parallax]'); } - var shouldAutoplay = !reducedMotion && (el.dataset.autoplay === '1' || kind === 'notice'); - if (shouldAutoplay) { + if (!reducedMotion && (el.dataset.autoplay === '1' || kind === 'notice')) { options.autoplay = { delay: Number(el.dataset.interval || 4000), disableOnInteraction: false, pauseOnMouseEnter: true }; } try { el.aksrtSwiper = new window.Swiper(el, options); - if (shouldAutoplay) initSwiperPause(el, el.aksrtSwiper); } catch (error) { el.classList.add('is-fallback'); } @@ -216,7 +161,6 @@ if (!widget || !instance || !config.restUrl || container.dataset.loading === '1') return; container.dataset.loading = '1'; container.classList.add('is-loading'); - container.setAttribute('aria-busy', 'true'); fetch(itemsUrl(widget, instance, page, segment), { credentials: 'same-origin', headers: { Accept: 'application/json' } }) .then(function (response) { if (!response.ok) throw new Error('items'); return response.json(); }) .then(function (data) { @@ -236,11 +180,7 @@ scan(container); }) .catch(function () { container.innerHTML = errorMarkup(); }) - .finally(function () { - delete container.dataset.loading; - container.classList.remove('is-loading'); - container.setAttribute('aria-busy', 'false'); - }); + .finally(function () { delete container.dataset.loading; container.classList.remove('is-loading'); }); } function initItems(container) { @@ -267,17 +207,12 @@ function initTabs(tabs) { if (!once(tabs, 'Tabs')) return; - tabs.addEventListener('keydown', function (event) { moveTabFocus(event, tabs); }); tabs.addEventListener('click', function (event) { var button = event.target.closest('[data-segment]'); if (!button) return; - tabs.querySelectorAll('[role="tab"]').forEach(function (tab) { - tab.setAttribute('aria-selected', tab === button ? 'true' : 'false'); - tab.setAttribute('tabindex', tab === button ? '0' : '-1'); - }); + tabs.querySelectorAll('[role="tab"]').forEach(function (tab) { tab.setAttribute('aria-selected', tab === button ? 'true' : 'false'); }); var panel = tabs.querySelector('[data-widget-items]'); panel.dataset.segment = button.dataset.segment; - panel.setAttribute('aria-labelledby', button.id); loadItems(panel, 1, false); }); } @@ -301,15 +236,11 @@ function initFeatures(features) { if (!once(features, 'Features')) return; - features.addEventListener('keydown', function (event) { moveTabFocus(event, features); }); features.addEventListener('click', function (event) { var button = event.target.closest('[data-feature-index]'); if (!button) return; var index = button.dataset.featureIndex; - features.querySelectorAll('[data-feature-index]').forEach(function (tab) { - tab.setAttribute('aria-selected', tab === button ? 'true' : 'false'); - tab.setAttribute('tabindex', tab === button ? '0' : '-1'); - }); + features.querySelectorAll('[data-feature-index]').forEach(function (tab) { tab.setAttribute('aria-selected', tab === button ? 'true' : 'false'); }); features.querySelectorAll('[data-feature-panel]').forEach(function (panel) { panel.hidden = panel.dataset.featurePanel !== index; }); }); } diff --git a/src/WidgetSystem/AdminFormController.php b/src/WidgetSystem/AdminFormController.php index 019e966f..a461331b 100644 --- a/src/WidgetSystem/AdminFormController.php +++ b/src/WidgetSystem/AdminFormController.php @@ -28,8 +28,7 @@ final class AdminFormController { wp_enqueue_script('jquery-ui-sortable'); wp_enqueue_style('aksrt-widget-admin', AKSRT_URI . '/assets/widgets/admin.css', ['wp-color-picker'], AKSRT_VERSION); wp_enqueue_style('aksrt-widget-preview', AKSRT_URI . '/assets/widgets/frontend.css', [], AKSRT_VERSION); - wp_enqueue_script('aksrt-widget-admin-core', AKSRT_URI . '/assets/widgets/admin-core.js', [], AKSRT_VERSION, true); - wp_enqueue_script('aksrt-widget-admin', AKSRT_URI . '/assets/widgets/admin.js', ['jquery', 'jquery-ui-sortable', 'wp-color-picker', 'aksrt-widget-admin-core'], AKSRT_VERSION, true); + wp_enqueue_script('aksrt-widget-admin', AKSRT_URI . '/assets/widgets/admin.js', ['jquery', 'jquery-ui-sortable', 'wp-color-picker'], AKSRT_VERSION, true); wp_localize_script('aksrt-widget-admin', 'aksrtWidgetAdmin', [ 'ajaxUrl' => admin_url('admin-ajax.php'), 'action' => self::AJAX_ACTION, diff --git a/src/WidgetSystem/AssetManager.php b/src/WidgetSystem/AssetManager.php index 6d3fea58..9f05a93a 100644 --- a/src/WidgetSystem/AssetManager.php +++ b/src/WidgetSystem/AssetManager.php @@ -33,8 +33,6 @@ final class AssetManager { 'loading' => __('正在加载…', 'aksrt'), 'error' => __('加载失败,请重试。', 'aksrt'), 'retry' => __('重试', 'aksrt'), - 'pause' => __('暂停自动播放', 'aksrt'), - 'resume' => __('继续自动播放', 'aksrt'), ], ]; wp_add_inline_script('aksrt-widgets', 'window.aksrtWidgets=' . wp_json_encode($config) . ';', 'before'); diff --git a/src/WidgetSystem/FrontendRenderer.php b/src/WidgetSystem/FrontendRenderer.php index 232675e7..e77caaaf 100644 --- a/src/WidgetSystem/FrontendRenderer.php +++ b/src/WidgetSystem/FrontendRenderer.php @@ -9,7 +9,7 @@ defined('ABSPATH') || exit; */ final class FrontendRenderer { public static function render(WidgetDefinition $definition, array $args, array $instance, SchemaWidget $widget): void { - if (!WidgetVisibility::should_render($instance)) { + if (!self::is_visible($instance)) { return; } @@ -24,9 +24,10 @@ final class FrontendRenderer { AssetManager::enqueue_for($definition); $classes = ['aksrt-widget', 'aksrt-widget--' . $definition->slug(), 'aksrt-widget--' . $definition->size()]; - $device_class = WidgetVisibility::device_class($instance); - if ($device_class !== '') { - $classes[] = $device_class; + if (($instance['show_type'] ?? 'all') === 'only_pc') { + $classes[] = 'aksrt-widget--only-pc'; + } elseif (($instance['show_type'] ?? 'all') === 'only_sm') { + $classes[] = 'aksrt-widget--only-mobile'; } if (!empty($instance['sidebar_affix'])) { $classes[] = 'aksrt-widget--affix'; @@ -48,6 +49,17 @@ final class FrontendRenderer { echo (string) ($args['after_widget'] ?? ''); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } + private static function is_visible(array $instance): bool { + $type = (string) ($instance['show_id_type'] ?? ''); + $ids = RendererHelpers::ids((string) ($instance['show_ids'] ?? '')); + if ($type === '' || !$ids) { + return true; + } + $current = function_exists('is_singular') && is_singular() && function_exists('get_queried_object_id') ? (int) get_queried_object_id() : 0; + $matched = in_array($current, $ids, true); + return $type === 'show' ? $matched : !$matched; + } + private static function title(array $instance): string { $title = trim((string) ($instance['title'] ?? '')); if ($title === '') { diff --git a/src/WidgetSystem/Renderers/ContentRenderer.php b/src/WidgetSystem/Renderers/ContentRenderer.php index e2718c76..96f2f083 100644 --- a/src/WidgetSystem/Renderers/ContentRenderer.php +++ b/src/WidgetSystem/Renderers/ContentRenderer.php @@ -97,10 +97,10 @@ final class ContentRenderer { private static function posts(string $slug, array $instance, array $context): string { $widget_id = (string) ($context['widget_id'] ?? ''); if (($instance['load_mode'] ?? 'sync') === 'ajax') { - return '
'; + return ''; } $payload = PostRenderer::payload($instance, $slug, 1); - return ''; + return ''; } private static function tab_posts(array $instance, array $context): string { @@ -109,15 +109,11 @@ final class ContentRenderer { return RendererHelpers::empty_state(__('请配置文章栏目', 'aksrt')); } $buttons = ''; - $key = sanitize_html_class((string) ($context['widget_id'] ?? 'preview')) ?: 'preview'; - $panel_id = 'aksrt-tab-panel-' . $key; - $first_tab_id = 'aksrt-tab-' . $key . '-0'; foreach ($tabs as $index => $tab) { - $tab_id = 'aksrt-tab-' . $key . '-' . $index; - $buttons .= ''; + $buttons .= ''; } $payload = PostRenderer::payload($instance, 'tab_posts', 1, '0'); - return ''; + return ''; } private static function post_pay(array $instance): string { diff --git a/src/WidgetSystem/Renderers/GeneralRenderer.php b/src/WidgetSystem/Renderers/GeneralRenderer.php index b1e8020d..8f53bd8c 100644 --- a/src/WidgetSystem/Renderers/GeneralRenderer.php +++ b/src/WidgetSystem/Renderers/GeneralRenderer.php @@ -67,7 +67,7 @@ final class GeneralRenderer { return RendererHelpers::empty_state(__('请配置视频地址', 'aksrt')); } $ratio = (string) $i['ratio']; - return ''; + return ''; } private static function render_embed(array $i): string { diff --git a/src/WidgetSystem/Renderers/KnowledgeProductRenderer.php b/src/WidgetSystem/Renderers/KnowledgeProductRenderer.php index 936c2008..3bd2f181 100644 --- a/src/WidgetSystem/Renderers/KnowledgeProductRenderer.php +++ b/src/WidgetSystem/Renderers/KnowledgeProductRenderer.php @@ -9,13 +9,14 @@ defined('ABSPATH') || exit; /** Render the seven knowledge-product presentation widgets. */ final class KnowledgeProductRenderer { public static function render(string $slug, array $instance, array $context = []): string { + unset($context); switch ($slug) { case 'pricing_plans': return self::pricing($instance); case 'reviews': return self::reviews($instance); case 'faq': return self::faq($instance); case 'stats': return self::stats($instance); case 'highlights': return self::highlights($instance); - case 'feature_intro': return self::feature_intro($instance, $context); + case 'feature_intro': return self::feature_intro($instance); case 'tilt_cases': return self::tilt($instance); } return ''; @@ -95,20 +96,17 @@ final class KnowledgeProductRenderer { return $html . ''; } - private static function feature_intro(array $i, array $context): string { + private static function feature_intro(array $i): string { $tabs = $panels = ''; - $key = sanitize_html_class((string) ($context['widget_id'] ?? 'preview')) ?: 'preview'; foreach ((array) $i['items'] as $index => $item) { - $tab_id = 'aksrt-feature-' . $key . '-tab-' . $index; - $panel_id = 'aksrt-feature-' . $key . '-panel-' . $index; - $tabs .= ''; + $tabs .= ''; $features = ''; foreach (RendererHelpers::lines((string) ($item['features'] ?? '')) as $feature) { $features .= '' . esc_html((string) ($item['description'] ?? '')) . '
' . esc_html((string) ($item['description'] ?? '')) . '
'; }
-function aksrt_get_demo_link_html($post, string $class): string { unset($post); return 'DEMO'; }
-function is_user_logged_in(): bool { return (bool) $GLOBALS['aksrt_test_state']['logged_in']; }
-
-require dirname(__DIR__, 2) . '/vendor/autoload.php';
diff --git a/tests/widget-system/run.php b/tests/widget-system/run.php
deleted file mode 100644
index 94bb4edc..00000000
--- a/tests/widget-system/run.php
+++ /dev/null
@@ -1,217 +0,0 @@
-schema_contract();
- $this->query_contract();
- $this->visibility_contract();
- $this->transfer_round_trip();
- $this->rest_contract();
- $this->payment_states();
- if ($this->failures) {
- fwrite(STDERR, "Widget system tests failed:\n- " . implode("\n- ", $this->failures) . "\n");
- exit(1);
- }
- echo "Widget system tests passed: {$this->assertions} assertions across schema, query, visibility, transfer, REST and payment states.\n";
- }
-
- private function schema_contract(): void {
- $fields = [
- ['id' => 'mode', 'type' => 'select', 'default' => 'cards', 'options' => ['cards' => 'Cards', 'list' => 'List']],
- ['id' => 'count', 'type' => 'slider', 'default' => 3, 'min' => 1, 'max' => 8],
- ['id' => 'ids', 'type' => 'text', 'sanitize' => 'ids'],
- ['id' => 'link', 'type' => 'link'],
- ['id' => 'layout', 'type' => 'fieldset', 'fields' => [
- ['id' => 'enabled', 'type' => 'switcher', 'default' => false],
- ['id' => 'gap', 'type' => 'number', 'default' => 8, 'min' => 0, 'max' => 40],
- ]],
- ['id' => 'rows', 'type' => 'group', 'max_items' => 2, 'fields' => [
- ['id' => 'title', 'type' => 'text'],
- ['id' => 'tone', 'type' => 'radio', 'default' => 'blue', 'options' => ['blue' => 'Blue', 'red' => 'Red']],
- ]],
- ];
- $defaults = FieldSchema::defaults($fields);
- $this->same('cards', $defaults['mode'], 'schema defaults preserve enums');
- $this->same(['enabled' => false, 'gap' => 8], $defaults['layout'], 'nested defaults are recursive');
- $normalized = FieldSchema::normalize($fields, [
- 'mode' => 'invalid', 'count' => 99, 'ids' => '2, 2,7 bad -4',
- 'link' => ['url' => 'javascript:alert(1)', 'text' => '课程', 'target' => 'popup'],
- 'layout' => ['enabled' => 'yes', 'gap' => -9, 'unknown' => 'drop'],
- 'rows' => [
- ['title' => '第一项', 'tone' => 'red'],
- ['title' => '第二项', 'tone' => 'invalid'],
- ['title' => '第三项'],
- ],
- ]);
- $this->same('cards', $normalized['mode'], 'invalid enum falls back');
- $this->same(8, $normalized['count'], 'numeric value clamps to maximum');
- $this->same('2,7,4', $normalized['ids'], 'ID list is unique and normalized');
- $this->same(['url' => '', 'text' => '课程', 'target' => ''], $normalized['link'], 'link parts sanitize independently');
- $this->same(['enabled' => true, 'gap' => 0], $normalized['layout'], 'nested values clamp and drop unknown keys');
- $this->same(2, count($normalized['rows']), 'group max_items is enforced');
- $this->same('blue', $normalized['rows'][1]['tone'], 'nested enum fallback is enforced');
-
- $definition = new WidgetDefinition([
- 'slug' => 'dependency_test', 'name' => 'Dependency Test',
- 'common' => ['title' => false, 'affix' => false, 'visibility' => false, 'animation' => false, 'layout' => false],
- 'fields' => [
- ['id' => 'controller', 'type' => 'select', 'default' => 'off', 'options' => ['off' => 'Off', 'on' => 'On']],
- ['id' => 'dependent', 'type' => 'text', 'dependency' => ['controller', '==', 'on']],
- ],
- ]);
- $widget = new SchemaWidget($definition);
- ob_start();
- $widget->render_full_form(['controller' => 'on', 'dependent' => 'visible']);
- $html = (string) ob_get_clean();
- $this->contains('data-dependency=', $html, 'form exports dependency rule');
- $this->contains('"controller"', $html, 'dependency JSON is safely escaped');
- }
-
- private function query_contract(): void {
- $args = PostQuery::arguments([
- 'count' => 99, 'category_ids' => '2,5', 'category_id' => 9, 'tag_ids' => '8,13',
- 'limit_day' => 14, 'orderby' => 'downloads', 'order' => 'asc', 'pay_type' => 'gallery',
- ], 0);
- $this->same(50, $args['posts_per_page'], 'query count clamps');
- $this->same(1, $args['paged'], 'query page clamps');
- $this->same([9], $args['category__in'], 'segment category overrides category list');
- $this->same([8, 13], $args['tag__in'], 'query tag filter is normalized');
- $this->same('ASC', $args['order'], 'query order allowlist is applied');
- $this->same('meta_value_num', $args['orderby'], 'metric ordering uses numeric meta');
- $this->contains('gallery', json_encode($args['meta_query']), 'DomainModel pay type reaches meta query');
- $invalid = PostQuery::arguments(['pay_type' => '6', 'orderby' => 'drop table'], 1);
- $this->false(isset($invalid['meta_query']), 'legacy numeric pay types are rejected');
- $this->same('date', $invalid['orderby'], 'unknown orderby falls back');
- $tabs = ['tabs' => [['title' => 'A', 'category_id' => 2], ['title' => 'B', 'category_id' => 3]]];
- $this->same(3, PostQuery::segment($tabs, '1')['category_id'], 'valid tab segment resolves');
- $this->same(2, PostQuery::segment($tabs, '99')['category_id'], 'invalid tab segment falls back to first');
- }
-
- private function visibility_contract(): void {
- $this->true(WidgetVisibility::should_render([], 0, false), 'unrestricted widget renders');
- $this->true(WidgetVisibility::should_render(['show_id_type' => 'show', 'show_ids' => '3,8'], 8, true), 'show list matches singular ID');
- $this->false(WidgetVisibility::should_render(['show_id_type' => 'show', 'show_ids' => '3,8'], 4, true), 'show list rejects other IDs');
- $this->false(WidgetVisibility::should_render(['show_id_type' => 'show', 'show_ids' => '3,8'], 0, false), 'show list rejects archives');
- $this->false(WidgetVisibility::should_render(['show_id_type' => 'hide', 'show_ids' => '3,8'], 3, true), 'hide list suppresses matching ID');
- $this->true(WidgetVisibility::should_render(['show_id_type' => 'hide', 'show_ids' => '3,8'], 0, false), 'hide list keeps archives');
- $this->same('aksrt-widget--only-pc', WidgetVisibility::device_class(['show_type' => 'only_pc']), 'PC device class is stable');
- $this->same('aksrt-widget--only-mobile', WidgetVisibility::device_class(['show_type' => 'only_sm']), 'mobile device class is stable');
- $this->same('', WidgetVisibility::device_class(['show_type' => 'invalid']), 'invalid device rule adds no class');
- }
-
- private function transfer_round_trip(): void {
- aksrt_test_reset_state();
- $GLOBALS['wp_registered_sidebars'] = ['home_sidebar' => [], 'single_sidebar' => []];
- update_option('widget_aksrt_widget_posts', [2 => ['title' => '课程列表', 'count' => 999, 'style' => 'mini'], '_multiwidget' => 1]);
- $GLOBALS['aksrt_test_state']['sidebars'] = ['home_sidebar' => ['text-1', 'aksrt_widget_posts-2']];
- $payload = WidgetTransfer::export('home_sidebar');
- $this->same(WidgetTransfer::FORMAT, $payload['format'], 'export format is versioned');
- $this->same(1, count($payload['sidebars']['home_sidebar']), 'export ignores non-schema widgets');
- $this->same(50, $payload['sidebars']['home_sidebar'][0]['settings']['count'], 'export normalizes saved settings');
- update_option('widget_aksrt_widget_posts', []);
- $GLOBALS['aksrt_test_state']['sidebars']['single_sidebar'] = ['text-9'];
- $result = WidgetTransfer::import($payload, 'append', 'single_sidebar');
- $this->same(['sidebars' => 1, 'widgets' => 1], $result, 'append import reports changes');
- $this->same(['text-9', 'aksrt_widget_posts-1'], $GLOBALS['aksrt_test_state']['sidebars']['single_sidebar'], 'append import preserves existing widgets');
- $round_trip = WidgetTransfer::export('single_sidebar');
- $this->same($payload['sidebars']['home_sidebar'][0], $round_trip['sidebars']['single_sidebar'][0], 'export/import round trip preserves normalized module');
- $replacement = WidgetTransfer::template_payload('links_navigation');
- $result = WidgetTransfer::import($replacement, 'replace', 'single_sidebar');
- $this->same(3, $result['widgets'], 'targeted template import combines template areas');
- $this->false(in_array('text-9', $GLOBALS['aksrt_test_state']['sidebars']['single_sidebar'], true), 'replace import removes previous assignment');
- try {
- WidgetTransfer::import(['format' => 'zibll.widgets', 'sidebars' => []]);
- $this->failures[] = 'transfer rejects legacy formats: no exception thrown';
- } catch (InvalidArgumentException $error) {
- $this->contains(WidgetTransfer::FORMAT, $error->getMessage(), 'legacy import rejection names accepted format');
- }
- }
-
- private function rest_contract(): void {
- aksrt_test_reset_state();
- $controller = new RestController();
- $controller->register_routes();
- $this->same(2, count($GLOBALS['aksrt_test_state']['routes']), 'REST registers items and quote routes');
- $route = $GLOBALS['aksrt_test_state']['routes']['aksrt/v1/widgets/(?P