233 lines
12 KiB
JavaScript
233 lines
12 KiB
JavaScript
const test = require('node:test')
|
|
const assert = require('node:assert/strict')
|
|
const fs = require('node:fs')
|
|
const path = require('node:path')
|
|
const { spawnSync } = require('node:child_process')
|
|
|
|
const root = path.resolve(__dirname, '../..')
|
|
const read = file => fs.readFileSync(path.join(root, file), 'utf8')
|
|
const schema = read('src/Admin/Schema/SchemaPageDisplay.php')
|
|
const routes = read('assets/admin/src/constants/sections/page-display.js')
|
|
const controller = read('src/Admin/PageDisplayController.php')
|
|
const controllerBase = read('src/Admin/ControllerBase.php')
|
|
const provider = read('src/Admin/SchemaProvider.php')
|
|
const settingsFacade = read('src/Settings/Settings.php')
|
|
const scriptsPart1c = read('src/Core/ScriptsPart1c.php')
|
|
const footerCss = read('assets/css/frontend/footer/zibll-shell.css')
|
|
const headerMenuButtons = read('inc/helpers/aksrt-header-menu-buttons.php')
|
|
|
|
function activeRepositorySource() {
|
|
const roots = ['assets/admin/src', 'assets/js', 'inc', 'src', 'templates']
|
|
const rootFiles = ['404.php', 'archive.php', 'category.php', 'footer.php', 'functions.php', 'header.php', 'index.php', 'page.php', 'search.php', 'single.php', 'tag.php']
|
|
const files = [...rootFiles.filter(file => fs.existsSync(path.join(root, file)))]
|
|
const visit = relative => {
|
|
const absolute = path.join(root, relative)
|
|
for (const entry of fs.readdirSync(absolute, { withFileTypes: true })) {
|
|
const child = path.join(relative, entry.name)
|
|
if (entry.isDirectory()) visit(child)
|
|
else if (/\.(?:php|js|vue|css)$/.test(entry.name) && !entry.name.endsWith('.disabled')) files.push(child)
|
|
}
|
|
}
|
|
roots.forEach(visit)
|
|
return files.map(file => read(file)).join('\n')
|
|
}
|
|
|
|
function functionBody(name) {
|
|
const start = schema.indexOf(`private static function ${name}`)
|
|
assert.notEqual(start, -1, `missing schema builder ${name}`)
|
|
const next = schema.indexOf('\n\tprivate static function ', start + 1)
|
|
return schema.slice(start, next === -1 ? schema.length : next)
|
|
}
|
|
|
|
function topLevelFieldIds(name) {
|
|
return [...functionBody(name).matchAll(/self::field\('([^']+)'/g)].map(match => match[1])
|
|
}
|
|
|
|
test('page display exposes exactly eight ordered routes', () => {
|
|
const expected = ['header-nav', 'header-components', 'footer', 'homepage', 'category', 'tag', 'user-home', 'other']
|
|
const schemaOrder = [...schema.matchAll(/^\t\t\t\t'([^']+)'\s+=> self::/gm)].map(match => match[1])
|
|
assert.deepEqual(schemaOrder, expected)
|
|
const routeOrder = [...routes.matchAll(/^\s+\['([^']+)',/gm)].map(match => match[1])
|
|
assert.deepEqual(routeOrder, expected)
|
|
})
|
|
|
|
test('top-level field IDs match the Zibll V9.0 eight-page contract', () => {
|
|
assert.deepEqual(topLevelFieldIds('header_nav'), [
|
|
'header_theme_custom', 'header_theme_bg_custom', 'header_theme_bg', 'header_theme_color_custom',
|
|
'header_layout', 'mobile_header_layout', 'mobile_navbar_align', 'nav_fixed', 'nav_scroll_down_hide',
|
|
'nav_newposts', 'nav_newposts_radius', 'nav_newposts_class', 'nav_newposts_text',
|
|
'nav_user_newposts_btn', 'nav_user_newposts_btn_text',
|
|
])
|
|
assert.deepEqual(topLevelFieldIds('header_components'), [
|
|
'header_slider_show', 'header_slider_show_type', 'header_slider_nav_color', 'header_slider',
|
|
'header_slider_option', 'header_slider_search_s', 'header_slider_search_option',
|
|
'header_slider_card_s', 'header_slider_card_option', 'header_slider_filter_blur',
|
|
])
|
|
assert.deepEqual(topLevelFieldIds('footer'), [
|
|
'footer_theme_custom', 'footer_theme_bg_custom', 'footer_theme_bg', 'footer_theme_color_custom',
|
|
'fcode_template', 'footer_t1_m_s', 'footer_t1_img', 'footer_t1_img_dark', 'footer_t1_t',
|
|
'fcode_t1_code', 'fcode_t2_code_1', 'fcode_t2_code_2', 'footer_contact_m_s',
|
|
'footer_contact_wechat_img', 'footer_contact_qq', 'footer_contact_weibo', 'footer_contact_email',
|
|
'footer_contact_more', 'desc', 'icon', 'link', 'img', 'footer_mini_img_m_s', 'footer_mini_img',
|
|
'footer_mini_img_2', 'fcode_customize_code',
|
|
])
|
|
assert.deepEqual(topLevelFieldIds('homepage'), [
|
|
'home_posts_list_s', 'home_list1_orderby_s', 'home_list1_orderby_option', 'index_list_title',
|
|
'home_exclude_posts', 'home_exclude_cats', 'home_lists', 'title', 'term_id', 'orderby',
|
|
])
|
|
assert.deepEqual(topLevelFieldIds('category'), [
|
|
'page_cover_cat_s', 'cat_post_count_s', 'cat_default_cover', 'cat_orderby_s', 'cat_orderby_option',
|
|
'ajax_list_cat_cat', 'ajax_list_option_cat_cat', 'ajax_list_cat_tag', 'ajax_list_option_cat_tag',
|
|
])
|
|
assert.deepEqual(topLevelFieldIds('tag'), [
|
|
'tag_post_count_s', 'page_cover_tag_s', 'tag_default_cover', 'tag_orderby_s', 'tag_orderby_option',
|
|
'ajax_list_tag_cat', 'ajax_list_option_tag_cat', 'ajax_list_tag_tag', 'ajax_list_option_tag_tag',
|
|
])
|
|
assert.deepEqual(topLevelFieldIds('user_home'), ['author_main_tab_opt', 'author_main_tab_swiper'])
|
|
assert.deepEqual(topLevelFieldIds('other'), ['page_header_style', 'page_header_cover_img'])
|
|
})
|
|
|
|
test('defaults, nested structures, dependencies, and excluded product families are fixed', () => {
|
|
assert.match(schema, /'author_main_tab_opt'.*?'post'.*?'favorite'.*?'comment'.*?'follow'/s)
|
|
assert.match(schema, /'home_posts_list_s', '首页文章', 'switcher', true/)
|
|
assert.match(schema, /'page_header_style', '页面标题', 'radio', ''/)
|
|
assert.match(schema, /'author' => '公开用户主页'/)
|
|
assert.match(schema, /'user_center' => '用户中心'/)
|
|
assert.match(schema, /'header_slider_search_s'.*?'dependsOn' => self::not_empty\('header_slider_show'\)/)
|
|
assert.match(schema, /'header_slider_card_s'.*?'dependsOn' => self::not_empty\('header_slider_show'\)/)
|
|
assert.match(schema, /'header_slider_filter_blur'.*?'dependsOn' => self::not_empty\('header_slider_show'\)/)
|
|
assert.match(schema, /'all' => \[self::enabled\('home_posts_list_s'\), self::enabled\('home_list1_orderby_s'\)\]/)
|
|
for (const dead of ['nav_newposts_btns', 'nav_pay_vip', 'nav_user_pay_vip', 'forum_home', 'forum_plate', 'forum_post', 'ajax_list_cat_topics', 'ajax_list_tag_topics']) {
|
|
assert.equal(schema.includes(dead), false, `legacy/excluded field remains: ${dead}`)
|
|
}
|
|
assert.equal(headerMenuButtons.includes('nav_newposts_btns'), false)
|
|
assert.match(headerMenuButtons, /aksrt_render_new_add_btns\(\['post'\], \$class, \$label\)/)
|
|
})
|
|
|
|
test('schema REST saves are privileged, section-whitelisted, and deeply sanitized', () => {
|
|
assert.match(controllerBase, /current_user_can\('manage_options'\)/)
|
|
assert.match(controllerBase, /sanitize_payload\(\$domain, \$payload, \$section\)/)
|
|
assert.match(provider, /sanitize_object_fields/)
|
|
assert.match(provider, /validate_nested_field/)
|
|
assert.match(provider, /wp_kses_post/)
|
|
assert.match(provider, /esc_url_raw/)
|
|
assert.match(provider, /has an invalid value/)
|
|
assert.match(controller, /search\/\(\?P<source>posts\|categories\|tags\)/)
|
|
assert.match(controller, /'permission_callback' => \[\$this, 'check_permission'\]/)
|
|
})
|
|
|
|
test('legacy routes, components, and storage paths are gone', () => {
|
|
const active = [
|
|
read('src/Admin/PageDisplayController.php'),
|
|
read('src/Admin/GlobalController.php'),
|
|
read('assets/admin/src/api/modules/page-display.js'),
|
|
read('assets/admin/src/constants/sections/page-display.js'),
|
|
read('assets/admin/src/router/component-loaders/core.js'),
|
|
].join('\n')
|
|
for (const dead of ['/banners', '/settings/homepage', "'/settings/footer'", 'BannerManager', 'HomepageSettings', 'FooterSettings']) {
|
|
assert.equal(active.includes(dead), false, `legacy route/component remains: ${dead}`)
|
|
}
|
|
assert.equal(fs.existsSync(path.join(root, 'assets/admin/src/pages/content/BannerManager.vue')), false)
|
|
assert.equal(fs.existsSync(path.join(root, 'assets/admin/src/pages/content/HomepageSettings.vue')), false)
|
|
assert.equal(fs.existsSync(path.join(root, 'assets/admin/src/pages/content/FooterSettings.vue')), false)
|
|
assert.equal(fs.existsSync(path.join(root, 'src/Admin/HomepageSettingsRest.php')), false)
|
|
|
|
const repository = activeRepositorySource()
|
|
for (const dead of [
|
|
'aksrt_footer_settings', 'aksrt_banners', 'aksrt_banner', 'aksrt-banner', 'HomeModules', 'homepage_modules',
|
|
'/settings/homepage', 'home-modules', 'cms-home-modules', 'aksrt_preview', 'theme-background',
|
|
"templates/parts/home-modules",
|
|
]) {
|
|
assert.equal(repository.includes(dead), false, `active legacy implementation remains: ${dead}`)
|
|
}
|
|
assert.equal(/['"]\/settings\/footer['"]/.test(repository), false, 'standalone footer settings route remains')
|
|
|
|
for (const deleted of [
|
|
'src/Core/HomeModules.php',
|
|
'assets/js/cms-home-modules.js',
|
|
'assets/css/frontend/home-modules.css',
|
|
'assets/css/frontend/theme-background.css',
|
|
'assets/admin/src/utils/home-module-schema.js',
|
|
]) {
|
|
assert.equal(fs.existsSync(path.join(root, deleted)), false, `deleted implementation exists: ${deleted}`)
|
|
}
|
|
})
|
|
|
|
test('front-end page-display consumers read only the canonical section contracts', () => {
|
|
const category = read('category.php')
|
|
const tag = read('tag.php')
|
|
const termMeta = read('src/Core/TermMeta.php')
|
|
const footer = read('footer.php')
|
|
|
|
assert.equal(category.includes('get_post_filter_settings'), false)
|
|
assert.equal(category.includes('get_filter_config_for_category'), false)
|
|
assert.equal(tag.includes('get_post_filter_settings'), false)
|
|
assert.match(category, /ajax_category_enabled/)
|
|
assert.match(category, /orderby_enabled/)
|
|
assert.match(tag, /ajax_tag_enabled/)
|
|
assert.match(tag, /orderby_enabled/)
|
|
assert.match(termMeta, /get_category_page_settings/)
|
|
assert.match(termMeta, /get_tag_page_settings/)
|
|
assert.match(footer, /get_footer_template/)
|
|
assert.match(footer, /<ul class="flex ac gap20 footer-contact-box list-inline">/)
|
|
assert.match(footerCss, /\.footer \.list-inline\s*\{[^}]*display:\s*flex;[^}]*align-items:\s*center;[^}]*gap:\s*20px;/s)
|
|
assert.doesNotMatch(footerCss, /\.footer \.footer-miniimg\s*\{[^}]*display:\s*inline-block;/s)
|
|
assert.match(footerCss, /\.footer \.footer-miniimg img\s*\{[^}]*width:\s*90px;[^}]*margin:\s*0 6px;/s)
|
|
assert.match(settingsFacade, /public static function get_footer_template\(\): string\s*\{\s*return HomeCms::get_footer_template\(\);\s*\}/)
|
|
assert.equal(scriptsPart1c.includes('cartUrl'), false)
|
|
assert.doesNotMatch(scriptsPart1c, /\$runtime_routes\[['"]cart['"]\]/)
|
|
})
|
|
|
|
test('save contract rejects invalid input and round-trips canonical nested values', () => {
|
|
const result = spawnSync(process.env.PHP_BINARY || 'php', [path.join(__dirname, 'page-display-save-harness.php')], {
|
|
cwd: root,
|
|
encoding: 'utf8',
|
|
})
|
|
assert.equal(result.status, 0, result.stderr || result.stdout)
|
|
const data = JSON.parse(result.stdout)
|
|
|
|
assert.equal(data.permission_denied, true)
|
|
assert.equal(data.permission_allowed, true)
|
|
assert.equal(data.unknown_section, true)
|
|
assert.equal(data.invalid_enum_rejected, true)
|
|
assert.equal(data.nested_multiselect_valid, true)
|
|
assert.equal(data.footer_valid, true)
|
|
assert.equal(data.homepage_defaults.home_list1_orderby_option, '')
|
|
assert.equal(data.homepage_defaults.home_exclude_posts, '')
|
|
assert.equal(data.homepage_defaults.home_exclude_cats, '')
|
|
assert.equal(Object.hasOwn(data.footer_defaults.footer_mini_img[0], 'link'), false)
|
|
|
|
const slide = data.nested.header_slider[0]
|
|
assert.equal(slide.background, '')
|
|
assert.equal(Object.hasOwn(slide, 'unknown'), false)
|
|
assert.deepEqual(slide.link, { url: 'https://example.test/slide', target: '_blank', text: 'Go' })
|
|
assert.equal(slide.text.title, '<strong>Safe</strong>')
|
|
assert.equal(Object.hasOwn(slide.text, 'unknown'), false)
|
|
|
|
assert.equal(data.footer_clean.fcode_t1_code, '<strong>Allowed</strong>')
|
|
assert.equal(data.footer_clean.footer_contact_weibo, '')
|
|
assert.equal(Object.hasOwn(data.footer_clean, 'unknown_top_level'), false)
|
|
assert.deepEqual(data.footer_clean.footer_mini_img[0], {
|
|
text: 'QR',
|
|
image: 'https://example.test/qr.png',
|
|
link: { url: 'https://example.test/go', target: '_blank', text: 'Open' },
|
|
})
|
|
assert.equal(data.footer_readback.fcode_t1_code, '<strong>Allowed</strong>')
|
|
assert.deepEqual(data.footer_readback.footer_mini_img, data.footer_clean.footer_mini_img)
|
|
assert.deepEqual(data.visibility, {
|
|
disabled: false,
|
|
disabled_with_page_override: false,
|
|
page_enabled: true,
|
|
public_user_home: true,
|
|
user_center: true,
|
|
legacy_author_public_home: true,
|
|
generic_page_excludes_home: false,
|
|
generic_page_excludes_center: false,
|
|
templated_public_user_home: true,
|
|
templated_user_center: true,
|
|
home_excludes_page: false,
|
|
home_excludes_public_home: false,
|
|
home_excludes_user_center: false,
|
|
})
|
|
})
|