'use strict';
const assert = require('node:assert/strict');
const { spawnSync } = require('node:child_process');
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');
function enqueueBlocksFor(source, file) {
const uri = `/assets/js/${file}`;
const blocks = [];
let cursor = 0;
while (true) {
const uriAt = source.indexOf(uri, cursor);
if (uriAt === -1) break;
const start = source.lastIndexOf('wp_enqueue_script(', uriAt);
const end = source.indexOf(');', uriAt);
assert.notEqual(start, -1, `missing wp_enqueue_script() for ${file}`);
assert.notEqual(end, -1, `unterminated wp_enqueue_script() for ${file}`);
blocks.push(source.slice(start, end + 2));
cursor = uriAt + uri.length;
}
return blocks;
}
const lazyDefaults = {
lazy_posts_thumb: true,
lazy_avatar: true,
lazy_posts_content: false,
lazy_comment: true,
lazy_sider: true,
lazy_cover: true,
lazy_other: true,
lazy_animation: 'blur',
};
function phpHarness(body, values = {}, userAgent = 'Mozilla/5.0') {
const helper = path.join(root, 'inc/helpers/template-tags/pagination-permissions.php');
const code = `
define('ABSPATH', __DIR__);
$_SERVER['HTTP_USER_AGENT'] = ${JSON.stringify(userAgent)};
function add_filter() {}
function add_action() {}
function is_admin() { return false; }
function wp_doing_ajax() { return false; }
function sanitize_key($value) { return preg_replace('/[^a-z0-9_\\-]/', '', strtolower($value)); }
function esc_attr($value) { return htmlspecialchars((string) $value, ENT_QUOTES); }
function esc_url($value) { return (string) $value; }
function get_template_directory_uri() { return 'https://theme.test'; }
function aksrt_get_default_avatar_url() { return 'https://theme.test/default-avatar.png'; }
eval('namespace AKSRT\\\\Settings; class Settings { public static $values = []; public static function get_value($key, $default = null) { return array_key_exists($key, self::$values) ? self::$values[$key] : $default; } }');
\\AKSRT\\Settings\\Settings::$values = json_decode(${JSON.stringify(JSON.stringify(values))}, true);
require ${JSON.stringify(helper)};
${body}
`;
const result = spawnSync('php', ['-r', code], { cwd: root, encoding: 'utf8' });
assert.equal(result.status, 0, result.stderr || result.stdout);
return result.stdout.trim();
}
function sourceFiles(directory) {
const output = [];
for (const entry of fs.readdirSync(directory, { withFileTypes: true })) {
if (entry.name === '.git' || entry.name === '.claude' || entry.name === 'build' || entry.name === 'vendor' || entry.name === 'runtime' || entry.name === 'node_modules' || entry.name === 'third-party') continue;
if (/\.min\.(?:js|css)$/.test(entry.name)) continue;
const full = path.join(directory, entry.name);
if (entry.isDirectory()) output.push(...sourceFiles(full));
else if (/\.(?:php|js|vue|css)$/.test(entry.name)) output.push(full);
}
return output;
}
test('admin schemas expose the ten legacy lazy fields and defaults', () => {
const php = read('src/Admin/Schema/GlobalSchema.php');
const vue = read('assets/admin/src/schema/sections/global/common.js');
for (const [key, value] of Object.entries(lazyDefaults)) {
const literal = typeof value === 'string' ? `'${value}'` : String(value);
assert.match(php, new RegExp(`'key'\\s*=>\\s*'${key}'[\\s\\S]*?'default'\\s*=>\\s*${literal}`));
assert.match(vue, new RegExp(`${key}:\\s*${literal}`));
}
assert.match(php, /'key'\s*=>\s*'thumbnail'[\s\S]*?thumbnail\.svg/);
assert.match(vue, /thumbnail:\s*defaultThumbnail/);
});
test('PHP helpers emit placeholder/data-src, direct src, and crawler bypass', () => {
const enabled = phpHarness("echo aksrt_get_lazy_attr('lazy_posts_thumb', 'https://cdn.test/post.webp', 'fit-cover');");
assert.match(enabled, /class="lazyload fit-cover"/);
assert.match(enabled, /src="https:\/\/theme\.test\/assets\/img\/thumbnail\.svg"/);
assert.match(enabled, /data-src="https:\/\/cdn\.test\/post\.webp"/);
const disabled = phpHarness("echo aksrt_get_lazy_attr('lazy_posts_thumb', 'https://cdn.test/post.webp', 'fit-cover');", { lazy_posts_thumb: false });
assert.equal(disabled, 'class="fit-cover" src="https://cdn.test/post.webp"');
const crawler = phpHarness("echo aksrt_get_lazy_attr('lazy_posts_thumb', 'https://cdn.test/post.webp', 'fit-cover');", {}, 'Googlebot/2.1');
assert.equal(crawler, 'class="fit-cover" src="https://cdn.test/post.webp"');
const empty = phpHarness("echo aksrt_get_lazy_attr('lazy_other', '', 'media');");
assert.equal(empty, 'class="media" src=""');
});
test('content, avatar, and responsive image paths follow one contract', () => {
const content = phpHarness(`
$html = '
';
echo aksrt_lazy_content_images($html);
`, { lazy_posts_content: true });
assert.match(content, /thumbnail-lg\.svg/);
assert.match(content, /data-src="https:\/\/cdn\.test\/photo\.jpg"/);
assert.match(content, /data-srcset="https:\/\/cdn\.test\/photo-2x\.jpg 2x"/);
assert.doesNotMatch(content, /\ssrcset=/);
const avatar = phpHarness("echo aksrt_lazy_avatar_html('
');");
assert.match(avatar, /class="lazyload avatar"/);
assert.match(avatar, /src="https:\/\/theme\.test\/default-avatar\.png"/);
assert.match(avatar, /data-src="https:\/\/cdn\.test\/avatar\.png"/);
assert.doesNotMatch(avatar, /loading=/);
});
test('the legacy runtime owns lazySizes while AKSRT consumers share one image-attribute bridge', () => {
const runtime = read('assets/js/runtime/main.js');
const bridge = read('assets/js/lazy-image-attr.js');
const scripts = read('src/Core/ScriptsPart1c.php');
const routeScripts = read('src/Core/ScriptsPart1b.php');
const builder = read('tools/build-public-assets.mjs');
const loader = read('assets/js/runtime/loader.js');
const searchShell = read('inc/helpers/header/aksrt-header-menu-search.php');
const pagination = read('assets/js/core/pagination.js');
const widgets = read('assets/widgets/frontend.js');
assert.match(runtime, /window\.lazySizes = lazySizes/);
assert.match(bridge, /window\.aksrtGetLazyImageAttr = function/);
assert.match(builder, /assets\/js\/lazy-image-attr\.js/);
assert.match(builder, /assets\/js\/rehydrate\.js/);
assert.match(builder, /assets\/js\/runtime\/main\.min\.js/);
assert.doesNotMatch(loader, /['"]main['"]\s*:|tbquire\(\[['"]main['"]\]\)/);
assert.match(scripts, /assets\/js\/build\/public\.js'[\s\S]*?\['jquery'\]/);
for (const file of ['pay-vip.js', 'order-center.js', 'page-navs.js', 'credits.js']) {
const blocks = enqueueBlocksFor(routeScripts, file);
assert.ok(blocks.length > 0, `${file} must be enqueued`);
for (const block of blocks) {
assert.match(block, /aksrt-runtime/, `${file} must depend on the bundled image-attribute bridge`);
}
}
assert.match(runtime, /lazybeforeunveil/);
assert.match(runtime, /getElementsByTagName\('source'\), handleSources/);
assert.match(runtime, /var bg = _this\.attr\('data-bg'\)[\s\S]*?background-image/);
assert.match(runtime, /var lazyload_action = _this\.attr\('lazyload-action'\)/);
assert.match(runtime, /if \(_this\.attr\('remote-box'\)\)[\s\S]*?_this\.click\(\)/);
assert.match(searchShell, /'type'\s*=>\s*'click'/);
assert.match(runtime, /aksrt:remote-loaded/);
assert.doesNotMatch(runtime, /action:\s*'get_current_user'/);
assert.match(runtime, /var cases = jQuery\('\.vanilla-tilt:not\(\.is-initialized\)'\);[\s\S]*?if \(!cases\.length\)[\s\S]*?tbquire\(\['tilt'\]/);
assert.doesNotMatch(pagination, /IntersectionObserver|new Image\(/);
assert.doesNotMatch(widgets, /IntersectionObserver/);
});
test('post lists have no second lazy switch and keep legacy fallback thumbnails', () => {
const config = read('inc/helpers/posts-renderer/config.php');
const postData = read('inc/helpers/posts-renderer/post-data.php');
const markup = read('inc/helpers/posts-renderer/markup.php');
const phpSchema = read('src/Admin/Schema/SchemaPostList.php');
const vueSchema = read('assets/admin/src/schema/sections/post-list/thumb.js');
const legacyKey = 'thumb_' + 'lazy';
for (const source of [config, postData, markup, phpSchema, vueSchema]) {
assert.equal(source.includes(legacyKey), false);
}
assert.match(postData, /aksrt_get_lazy_attr\('lazy_posts_thumb', \(string\) \$data\['primary'\], 'fit-cover radius8'\)/);
assert.match(markup, /aksrt_get_lazy_attr\('lazy_posts_thumb', \(string\) \$data\['primary'\], 'fit-cover radius8'\)/);
assert.match(config, /\$primary = aksrt_renderer_spare_image\(\$thumb\);[\s\S]*?\$primary = \(string\) aksrt_get_lazy_thumb\(\);/);
assert.doesNotMatch(config, /rand_thumb/);
});
test('legacy lazy identifiers and theme-native loading attributes are gone', () => {
const files = sourceFiles(root).filter((file) => !file.endsWith('lazyload-contract.test.cjs'));
const source = files.map((file) => fs.readFileSync(file, 'utf8')).join('\n');
const forbidden = [
'data-' + 'lazy-animation',
'lazyload_' + 'enabled',
'get_' + 'lazyload_config',
'aksrt_' + 'lazy_img_attrs',
'aksrt_' + 'lazy_loading',
'thumb_' + 'lazy',
];
for (const token of forbidden) assert.equal(source.includes(token), false, token);
assert.doesNotMatch(source, /aksrt-lazy(?!sizes|-image-attr)/);
assert.doesNotMatch(source, /aksrt-loaded/);
assert.doesNotMatch(source, /loading=["'](?:lazy|eager)["']/);
assert.doesNotMatch(source, /\.loading\s*=\s*["'](?:lazy|eager)["']/);
assert.doesNotMatch(source, /setAttribute\(\s*["']loading["']/);
});