Files
wordpress-theme-aksrt/tests/widget-system/page-display-contract.test.cjs
T

372 lines
22 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 paymentSchema = read('src/Admin/Schema/SchemaPayment.php')
const adminVipSchema = read('assets/admin/src/schema/sections/payment/vip.js')
const paymentRuntime = read('src/Settings/PaymentRuntime.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/shell.css')
const frontendBaseCss = read('assets/css/frontend/base.css')
const headerMenuButtons = read('inc/helpers/aksrt-header-menu-buttons.php')
const frontendRenderer = read('src/WidgetSystem/FrontendRenderer.php')
const userRenderer = read('src/WidgetSystem/Renderers/UserRenderer.php')
const headerNavbar = read('inc/helpers/header/aksrt-header-navbar.php')
const headerUserbox = read('inc/helpers/aksrt-header-userbox.php')
const headerSearch = read('inc/helpers/header/aksrt-header-menu-search.php')
const headerCss = read('assets/css/frontend/header-userbox.css')
const vipCardRenderer = read('src/Content/VipServiceModal.php')
const vipModalCss = read('assets/css/frontend/vip-modal.css')
const payVipJs = read('assets/js/pay-vip.js')
const modalRuntimeJs = read('assets/js/core/modal-runtime.js')
const legacyMainCss = read('assets/css/main.css')
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 legacy 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',
])
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', '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('VIP navigation purchase switches are saved and consumed end to end', () => {
for (const key of ['nav_pay_vip', 'nav_user_pay_vip']) {
assert.match(paymentSchema, new RegExp(`'key' => '${key}'[^\\n]+?'type' => 'switch'[^\\n]+?'default' => true`))
assert.match(adminVipSchema, new RegExp(`${key}: true`))
assert.match(adminVipSchema, new RegExp(`key: '${key}'[^\\n]+type: 'switch'[^\\n]+default: true`))
assert.match(paymentRuntime, new RegExp(`'${key}'[^\\n]+!isset\\(\\$settings\\['${key}'\\]\\)[^\\n]+!empty\\(\\$settings\\['${key}'\\]\\)`))
}
assert.match(headerMenuButtons, /!empty\(\$vs\['enabled'\]\) && !empty\(\$vs\['nav_pay_vip'\]\)/)
assert.match(headerUserbox, /empty\(\$vip\['nav_user_pay_vip'\]\)[\s\S]*?return '';/)
assert.match(headerNavbar, /!empty\(\$vs\['enabled'\]\) && !empty\(\$vs\['nav_user_pay_vip'\]\)/)
})
test('header VIP hover cards match the legacy markup and style contract', () => {
assert.match(headerMenuButtons, /<span class="hover-show inline-block navbar-vip-hover">/)
assert.match(headerMenuButtons, /<div class="hover-show-con sub-menu sub-vip-card">/)
assert.match(vipCardRenderer, /aksrt_get_lazy_attr\('lazy_other', \$icon_url, 'img-icon'\)/)
assert.match(vipCardRenderer, /<div class=\"abs-center vip-baicon\"[^>]*>/)
assert.match(vipCardRenderer, /<div class=\"vip-img abs-right\">/)
assert.match(vipCardRenderer, /<div class=\"vip-name mb10\"><span class=\"mr6\">/)
assert.match(vipCardRenderer, /<ul class=\"mb10 relative\">/)
assert.doesNotMatch(vipCardRenderer, /vip-name__(?:icon|label)/)
assert.doesNotMatch(vipCardRenderer, /aksrt_format_vip_equity_html\(\$equity, 'mb10 relative'\)/)
assert.doesNotMatch(vipModalCss, /Header hover VIP cards/)
assert.doesNotMatch(vipModalCss, /\.navbar-vip-hover\.hover-show/)
assert.doesNotMatch(vipModalCss, /\.sub-vip-card/)
assert.doesNotMatch(vipModalCss, /\.vip-card\.vip-theme[12]/)
assert.match(legacyMainCss, /\.sub-vip-card\s*\{[^}]*font-size:\s*12px;[^}]*padding:\s*0\s*!important;/s)
assert.match(legacyMainCss, /\.sub-vip-card \.vip-card\s*\{[^}]*display:\s*inline-block;[^}]*width:\s*240px;[^}]*margin:\s*5px;/s)
assert.match(legacyMainCss, /\.sub-vip-card \.vip-baicon\.abs-center\s*\{[^}]*font-size:\s*10em;[^}]*left:\s*55%;/s)
assert.match(legacyMainCss, /\.sub-vip-card \.vip-img\s*\{[^}]*display:\s*none;/s)
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-actions \.navbar-vip-hover\s*\{[^}]*align-self:\s*stretch;[^}]*overflow:\s*visible;/s)
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-actions \.navbar-vip-hover > \.sub-vip-card\s*\{[^}]*top:\s*100%;[^}]*right:\s*0;[^}]*left:\s*auto;[^}]*max-width:\s*calc\(100vw - 24px\);/s)
})
test('VIP purchase modal matches legacy desktop-center and mobile-bottom positioning', () => {
const purchaseModal = payVipJs.slice(payVipJs.indexOf('function vipPay('), payVipJs.indexOf('function onDocClick('))
assert.doesNotMatch(purchaseModal, /mobileBottom:\s*false/)
assert.match(modalRuntimeJs, /options\.mobileBottom !== false && isMobile\(\)/)
assert.match(vipModalCss, /@media \(max-width: 768px\)[\s\S]*?\.aksrt-vip-modal-shell\.aksrt-modal-runtime--bottom \.aksrt-modal-runtime__dialog\s*\{[\s\S]*?width:\s*100%;[\s\S]*?max-height:\s*calc\(100vh - 20px\);[\s\S]*?border-radius:\s*16px 16px 0 0;/)
})
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('footer brand icons keep the Brands font when legacy fa prefix is present', () => {
const footer = read('footer.php')
const icons = read('inc/helpers/aksrt-icons.php')
assert.match(footer, /class="fa fab fa-weixin"/)
assert.match(footer, /class="fa fab fa-qq"/)
assert.match(footer, /class="fa fab fa-weibo"/)
assert.match(frontendBaseCss, /\.fa\.fab\s*\{[^}]*font-family:\s*"Font Awesome 5 Brands"[^}]*font-weight:\s*400/s)
assert.match(icons, /'wechat'\s*=>\s*'fa fab fa-weixin'/)
})
test('header multifunction search keeps focus and submit alignment route-independent', () => {
assert.match(
frontendBaseCss,
/\.header-slider-search \.line-form-input:focus,[\s\S]*?\.header-slider-search \.line-form-input:focus-visible\s*\{[^}]*border:\s*0;[^}]*outline:\s*0;[^}]*box-shadow:\s*none;/,
)
assert.match(
frontendBaseCss,
/\.header-slider-search \.line-form \.abs-right\s*\{[^}]*position:\s*absolute;[^}]*right:\s*0\.7em;[^}]*top:\s*50%;[^}]*transform:\s*translateY\(-50%\);[^}]*margin-top:\s*0;/,
)
})
test('user profile widget owns its only card surface and keeps membership below the name', () => {
assert.match(frontendRenderer, /size\(\) === 'sidebar' && \$definition->slug\(\) !== 'user_card'/)
assert.match(userRenderer, /display_name\) \. '<\/a><\/h3>'/)
assert.match(userRenderer, /aksrt-user-card__identity.*aksrt_render_user_identity_chips\(\$user_id, 'widget'\)/s)
assert.doesNotMatch(userRenderer, /display_name\) \. '<\/a>' \. \(function_exists\('aksrt_render_vip_badge'/)
assert.match(userRenderer, /\$variant = \$primary \? ' is-primary' : ' is-secondary'/)
// 未登录卡片复刻 zibll user-card 结构(zib_widget_ui_user + zib_get_user_singin_page_box)
assert.match(userRenderer, /user-card widget/)
assert.match(userRenderer, /signin-loader but jb-blue/)
assert.match(userRenderer, /signup-loader but jb-yellow/)
assert.doesNotMatch(userRenderer, /aksrt-user-card is-guest/)
assert.doesNotMatch(userRenderer, /aksrt-widget-button is-ghost/)
})
test('desktop header actions share one row without generic button masks', () => {
assert.match(headerNavbar, /aksrt-navbar-actions navbar-form navbar-right/)
assert.match(headerNavbar, /class="aksrt-nav-icon nav-search-btn main-search-btn"/)
assert.doesNotMatch(headerNavbar, /class="btn nav-search-btn main-search-btn"/)
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-actions\s*\{[^}]*display:\s*flex;[^}]*align-items:\s*center;/s)
assert.match(headerCss, /\.navbar-top \.aksrt-nav-icon,[\s\S]*?background:\s*transparent !important;/)
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-actions \.navbar-cta\.radius\s*\{[^}]*border-radius:\s*50px;/s)
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-actions \.navbar-cta > \.icon\s*\{[^}]*width:\s*1\.2em;[^}]*height:\s*1\.2em;[^}]*flex:\s*0 0 auto;/s)
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-account > li\s*\{[^}]*position:\s*relative;/s)
assert.match(headerCss, /\.navbar-top \.aksrt-navbar-account > li > \.sub-menu\s*\{[^}]*top:\s*calc\(100% \+ 5px\);[^}]*right:\s*0;/s)
})
test('search dropdown clears the fixed header and closes from the panel lower-right', () => {
assert.match(headerCss, /\.navbar-search\s*\{[^}]*z-index:\s*1001;[^}]*transform:\s*translate3d\(0, -24px, 0\);[^}]*transition:\s*transform \.24s ease, opacity \.18s ease, visibility 0s linear \.24s;/s)
assert.match(headerCss, /\.navbar-search\.show\s*\{[^}]*opacity:\s*1;[^}]*transform:\s*translate3d\(0, 0, 0\);/s)
assert.doesNotMatch(headerCss, /\.navbar-search\s*\{[^}]*height:\s*100%/s)
assert.doesNotMatch(headerCss, /\.navbar-search > \.container\s*\{[^}]*overflow-y:\s*auto/s)
assert.match(headerCss, /\.navbar-search \.line-form-input:focus,[\s\S]*?\.navbar-search \.line-form-input:focus-visible\s*\{[^}]*outline:\s*0;[^}]*box-shadow:\s*none;/s)
assert.match(headerCss, /\.navbar-search \.search-close-box\s*\{[^}]*display:\s*flex;[^}]*justify-content:\s*flex-end;[^}]*margin:\s*16px auto 0;/s)
assert.doesNotMatch(headerCss, /\.navbar-search \.search-close-box\s*\{[^}]*position:\s*fixed/s)
assert.ok(headerSearch.indexOf('echo $remote') < headerSearch.indexOf('search-close-box'))
})
test('header user-card publish controls are removed from configuration and runtime', () => {
assert.doesNotMatch(schema, /nav_user_newposts_btn/)
assert.doesNotMatch(headerNavbar, /nav_user_newposts_btn/)
assert.match(headerNavbar, /esc_html__\('发布内容', 'aksrt'\)/)
})
test('header user-card membership stays on one row with a date-only expiry', () => {
assert.match(headerUserbox, /wp_date\('Y年m月d日', \$expires\)/)
assert.doesNotMatch(headerUserbox, /wp_date\('Y-m-d H:i', \$expires\)/)
assert.match(headerCss, /\.aksrt-header-payvip__active\s*\{[^}]*flex-direction:\s*row;[^}]*gap:\s*8px !important;[^}]*white-space:\s*nowrap;/s)
assert.match(headerCss, /\.aksrt-header-payvip__exp\s*\{[^}]*display:\s*inline;[^}]*white-space:\s*nowrap;/s)
})
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.vip_switches_valid, true)
assert.deepEqual(data.vip_switches_clean, {
nav_pay_vip: false,
nav_user_pay_vip: true,
})
assert.deepEqual(data.vip_switches_readback, {
nav_pay_vip: false,
nav_user_pay_vip: 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,
})
})