'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["']/); });