# 设计文档

## 概述

本设计文档针对 Argon 主题的 PJAX 和 LazyLoad 功能进行优化。通过深入分析 argontheme.js（3700+ 行）的实际实现，我们识别出了重复初始化、资源泄漏、性能问题等关键缺陷。

**优化目标：**
- 消除 PJAX 事件处理中的重复初始化调用
- 完善 LazyLoad Observer 的生命周期管理
- 优化图片加载效果，使用 requestAnimationFrame 替代 setTimeout
- 改进 GT4 验证码、Zoomify、Tippy 等第三方库的清理逻辑
- 优化滚动位置管理和移动端目录初始化

**设计原则：**
- 最小化修改：优先重构现有代码，避免大规模重写
- 向后兼容：保持现有 API 和配置选项不变
- 性能优先：减少不必要的 DOM 操作和重复计算
- 渐进增强：确保在旧浏览器上有合理的降级方案

## 架构

### 当前架构问题

**PJAX 事件流（当前）：**
```
pjax:click
  ↓
pjax:send (NProgress.set(0.618))
  ↓
pjax:beforeReplace
  ├─ 清理 lazyloadObserver
  ├─ 清理 zoomifyInstances
  └─ 清理 Tippy 实例
  ↓
pjax:complete
  ├─ waterflowInit()      ← 重复调用
  ├─ lazyloadInit()       ← 重复调用
  ├─ zoomifyInit()
  ├─ highlightJsRender()
  ├─ ... (10+ 个初始化函数)
  └─ 恢复滚动位置
  ↓
pjax:end
  ├─ setTimeout 100ms
  │   ├─ waterflowInit()  ← 重复调用
  │   └─ lazyloadInit()   ← 重复调用
  └─ GT4 验证码重置
```

**问题分析：**
1. `waterflowInit()` 和 `lazyloadInit()` 在 `pjax:complete` 和 `pjax:end` 中都被调用
2. `pjax:end` 使用 100ms 延迟，可能导致竞态条件
3. 资源清理在 `beforeReplace` 中进行，但某些资源可能在 `complete` 后才创建

### 优化后架构

**PJAX 事件流（优化后）：**
```
pjax:click
  ↓
pjax:send (NProgress.set(0.618))
  ↓
pjax:beforeReplace
  ├─ cleanupResources()
  │   ├─ 清理 lazyloadObserver
  │   ├─ 清理 zoomifyInstances
  │   ├─ 清理 Tippy 实例
  │   └─ 清理事件监听器
  └─ 更新 UI 状态
  ↓
pjax:complete
  ├─ initializePageResources()
  │   ├─ waterflowInit()
  │   ├─ lazyloadInit()
  │   ├─ zoomifyInit()
  │   ├─ highlightJsRender()
  │   └─ ... (其他初始化)
  └─ 恢复滚动位置
  ↓
pjax:end
  └─ finalizePageLoad()
      ├─ resetMobileCatalog()
      └─ resetGT4Captcha()
```

**优化要点：**
1. 将资源清理逻辑封装到 `cleanupResources()` 函数
2. 将初始化逻辑封装到 `initializePageResources()` 函数
3. 移除 `pjax:end` 中的重复初始化调用
4. 使用函数封装提高代码可维护性

## 组件和接口

### 1. 资源清理模块

**函数：`cleanupPjaxResources()`**

```javascript
/**
 * 清理 PJAX 页面切换前的所有资源
 * 在 pjax:beforeReplace 事件中调用
 */
function cleanupPjaxResources() {
	// 清理 LazyLoad Observer
	if (lazyloadObserver) {
		lazyloadObserver.disconnect();
		lazyloadObserver = null;
	}
	
	// 清理 Zoomify 实例
	if (zoomifyInstances && zoomifyInstances.length > 0) {
		zoomifyInstances.forEach(function(instance) {
			try {
				if (instance && typeof instance.destroy === 'function') {
					instance.destroy();
				}
			} catch(e) {
				console.warn('Failed to destroy Zoomify instance:', e);
			}
		});
		zoomifyInstances = [];
	}
	$('img.zoomify-initialized').removeClass('zoomify-initialized');
	
	// 清理 Tippy 实例
	if (typeof tippy !== 'undefined') {
		document.querySelectorAll('[data-tippy-root]').forEach(function(el) {
			try {
				if (el._tippy && typeof el._tippy.destroy === 'function') {
					el._tippy.destroy();
				}
			} catch(e) {
				console.warn('Failed to destroy Tippy instance:', e);
			}
		});
		$('.tippy-initialized').removeClass('tippy-initialized');
	}
}
```

### 2. LazyLoad 优化模块

**函数：`lazyloadInit()` 优化版**

```javascript
/**
 * 初始化图片懒加载
 * 优化：检查现有 Observer，使用 requestAnimationFrame 优化加载效果
 */
function lazyloadInit() {
	// 清理旧的 Observer（防御性编程）
	if (lazyloadObserver) {
		lazyloadObserver.disconnect();
		lazyloadObserver = null;
	}
	
	// 检查是否启用懒加载
	if (argonConfig.lazyload === false || argonConfig.lazyload === 'false') {
		loadAllImagesImmediately();
		return;
	}
	
	let images = document.querySelectorAll('img.lazyload[data-src]');
	if (images.length === 0) {
		return;
	}
	
	let effect = argonConfig.lazyload_effect || 'fadeIn';
	let threshold = parseInt(argonConfig.lazyload_threshold) || 800;
	
	// 使用 IntersectionObserver 实现懒加载
	if ('IntersectionObserver' in window) {
		lazyloadObserver = new IntersectionObserver(function(entries) {
			entries.forEach(function(entry) {
				if (entry.isIntersecting) {
					let img = entry.target;
					loadImageOptimized(img, effect);
					lazyloadObserver.unobserve(img);
				}
			});
		}, {
			rootMargin: threshold + 'px 0px'
		});
		
		images.forEach(function(img) {
			// 重置图片状态
			img.style.opacity = '';
			img.style.transform = '';
			img.style.transition = '';
			lazyloadObserver.observe(img);
		});
	} else {
		// 降级方案：使用滚动监听
		lazyloadFallback(images, effect, threshold);
	}
}

/**
 * 优化的图片加载函数
 * 使用 requestAnimationFrame 替代 setTimeout
 */
function loadImageOptimized(img, effect) {
	let src = img.getAttribute('data-src');
	if (!src) return;
	
	// 预加载图片
	let tempImg = new Image();
	tempImg.onload = function() {
		img.src = src;
		img.removeAttribute('data-src');
		img.classList.remove('lazyload');
		
		// 移除所有 lazyload-style-* 类
		img.className = img.className.replace(/\blazyload-style-\d+\b/g, '').trim();
		
		// 使用 requestAnimationFrame 应用加载效果
		applyLoadEffectOptimized(img, effect);
	};
	tempImg.onerror = function() {
		// 加载失败时使用原始 src
		img.src = src;
		img.removeAttribute('data-src');
		img.classList.remove('lazyload');
	};
	tempImg.src = src;
}

/**
 * 使用 requestAnimationFrame 应用加载效果
 */
function applyLoadEffectOptimized(img, effect) {
	if (effect === 'fadeIn') {
		img.style.opacity = '0';
		img.style.transition = 'opacity 0.3s ease';
		requestAnimationFrame(function() {
			requestAnimationFrame(function() {
				img.style.opacity = '1';
			});
		});
		// 清理 transition 样式
		setTimeout(function() {
			img.style.transition = '';
		}, 310);
	} else if (effect === 'slideDown') {
		img.style.opacity = '0';
		img.style.transform = 'translateY(-20px)';
		img.style.transition = 'opacity 0.3s ease, transform 0.3s ease';
		requestAnimationFrame(function() {
			requestAnimationFrame(function() {
				img.style.opacity = '1';
				img.style.transform = 'translateY(0)';
			});
		});
		// 清理样式
		setTimeout(function() {
			img.style.transition = '';
			img.style.transform = '';
		}, 310);
	}
}

/**
 * 降级方案：使用滚动监听实现懒加载
 */
function lazyloadFallback(images, effect, threshold) {
	let loadedImages = new Set();
	
	function checkImagesInView() {
		let viewportHeight = window.innerHeight;
		let scrollTop = document.documentElement.scrollTop || document.body.scrollTop;
		
		images.forEach(function(img) {
			if (loadedImages.has(img)) return;
			
			let rect = img.getBoundingClientRect();
			if (rect.top < viewportHeight + threshold && rect.bottom > -threshold) {
				loadImageOptimized(img, effect);
				loadedImages.add(img);
			}
		});
	}
	
	// 使用节流函数优化性能
	let throttleTimer = null;
	function throttledCheck() {
		if (throttleTimer) return;
		throttleTimer = setTimeout(function() {
			checkImagesInView();
			throttleTimer = null;
		}, 100);
	}
	
	// 绑定事件监听器
	window.addEventListener('scroll', throttledCheck, {passive: true});
	window.addEventListener('resize', throttledCheck, {passive: true});
	
	// 立即检查一次
	checkImagesInView();
}

/**
 * 立即加载所有图片（懒加载禁用时）
 */
function loadAllImagesImmediately() {
	let images = document.querySelectorAll('img.lazyload[data-src]');
	images.forEach(function(img) {
		let src = img.getAttribute('data-src');
		if (src) {
			img.src = src;
			img.removeAttribute('data-src');
			img.classList.remove('lazyload');
			img.className = img.className.replace(/\blazyload-style-\d+\b/g, '').trim();
		}
	});
}
```

### 3. PJAX 事件处理优化

**优化后的事件处理器：**

```javascript
$(document).on('pjax:beforeReplace', function(e, dom) {
	// 清理旧页面的资源
	cleanupPjaxResources();
	
	// 更新 UI 状态
	if ($('#post_comment', dom[0]).length > 0) {
		$('#fabtn_go_to_comment').removeClass('d-none');
	} else {
		$('#fabtn_go_to_comment').addClass('d-none');
	}
	
	// 处理滚动位置
	if ($('html').hasClass('banner-as-cover')) {
		if (!$('#main').hasClass('article-list-home')) {
			pjaxScrollTop = 0;
		}
	}
});

$(document).on('pjax:complete', function() {
	pjaxLoading = false;
	NProgress.inc();
	
	// MathJax 渲染
	try {
		if (typeof MathJax !== 'undefined') {
			if (MathJax.Hub !== undefined) {
				MathJax.Hub.Typeset();
			} else {
				MathJax.typeset();
			}
		}
	} catch (err) {
		console.warn('MathJax rendering failed:', err);
	}
	
	// KaTeX 渲染
	try {
		if (typeof renderMathInElement !== 'undefined') {
			renderMathInElement(document.body, {
				delimiters: [
					{left: '$', right: '$', display: true},
					{left: '$', right: '$', display: false},
					{left: '\\(', right: '\\)', display: false}
				]
			});
		}
	} catch (err) {
		console.warn('KaTeX rendering failed:', err);
	}
	
	// 初始化页面资源（只调用一次）
	waterflowInit();
	lazyloadInit();
	zoomifyInit();
	highlightJsRender();
	panguInit();
	clampInit();
	tippyInit();
	getGithubInfoCardContent();
	showPostOutdateToast();
	calcHumanTimesOnPage();
	foldLongComments();
	foldLongShuoshuo();
	$('html').trigger('resize');
	
	// 恢复滚动位置
	if (pjaxScrollTop > 0) {
		$('body,html').scrollTop(pjaxScrollTop);
		pjaxScrollTop = 0;
	}
	
	// 调用用户自定义回调
	if (typeof window.pjaxLoaded === 'function') {
		try {
			window.pjaxLoaded();
		} catch (err) {
			console.error('pjaxLoaded callback failed:', err);
		}
	}
	
	NProgress.done();
});

$(document).on('pjax:end', function() {
	// 只处理特殊的后置任务，不重复初始化
	
	// 重置移动端目录状态
	if (typeof window.resetMobileCatalog === 'function') {
		try {
			window.resetMobileCatalog();
		} catch (err) {
			console.warn('resetMobileCatalog failed:', err);
		}
	}
	
	// GT4: PJAX 后确保评论页验证码已初始化
	resetGT4Captcha();
});

/**
 * 重置 GT4 验证码
 */
function resetGT4Captcha() {
	try {
		if ($('#geetest-captcha').length > 0) {
			// 重置前端状态，避免重复提交阻塞
			window.geetestVerified = false;
			window.geetestAutoSubmitting = false;
			
			// 清空隐藏字段，防止残留导致 pass_token 复用
			$('#geetest_lot_number').val('');
			$('#geetest_captcha_output').val('');
			$('#geetest_pass_token').val('');
			$('#geetest_gen_time').val('');
			
			// 清空容器，防止重复 appendTo 导致多个实例
			$('#geetest-captcha').empty();
			
			// 若页面脚本已提供初始化方法，则调用以加载验证码
			if (typeof initGeetestCaptcha === 'function') {
				initGeetestCaptcha();
			} else if (typeof loadGeetestScript === 'function' && typeof initGeetestCaptchaCore === 'function') {
				loadGeetestScript(function() {
					initGeetestCaptchaCore();
				});
			}
		}
	} catch (e) {
		console.warn('Geetest init on PJAX failed:', e);
	}
}
```

### 4. 滚动位置管理优化

**优化后的滚动位置处理：**

```javascript
// 在 pjax:click 事件中设置滚动位置
$(document).on('pjax:click', function(e, options, g) {
	pjaxLoading = true;
	pjaxScrollTop = 0;
	
	// 根据链接类型决定滚动位置
	if ($('html').hasClass('banner-as-cover')) {
		if (g.is('.page-link')) {
			// 分页链接：滚动到内容区域
			pjaxScrollTop = $('#content').offset().top - 80;
		}
		// 其他链接：滚动到顶部（pjaxScrollTop = 0）
	}
});
```

## 数据模型

### 全局状态变量

```javascript
// LazyLoad 相关
var lazyloadObserver = null;  // IntersectionObserver 实例

// Zoomify 相关
var zoomifyInstances = [];    // Zoomify 实例数组

// PJAX 相关
var pjaxLoading = false;      // PJAX 加载状态
var pjaxScrollTop = 0;        // PJAX 后的滚动位置

// GT4 验证码相关
window.geetestVerified = false;
window.geetestAutoSubmitting = false;
```

### 配置对象

```javascript
argonConfig = {
	lazyload: true,              // 是否启用懒加载
	lazyload_effect: 'fadeIn',   // 加载效果：fadeIn | slideDown
	lazyload_threshold: 800,     // 提前加载阈值（px）
	headroom: 'true',            // Headroom 模式
	toolbar_blur: false,         // 顶栏模糊效果
	waterflow_columns: '2and3',  // 瀑布流列数
	// ... 其他配置
};
```

## 错误处理

### 错误处理策略

1. **资源清理错误**：使用 try-catch 包裹，记录警告但继续执行
2. **初始化错误**：捕获异常，输出错误信息，不阻塞其他功能
3. **第三方库错误**：检查库是否存在，使用可选链操作符
4. **降级方案**：不支持的功能提供降级实现

### 错误处理示例

```javascript
// 资源清理错误处理
function cleanupPjaxResources() {
	try {
		if (lazyloadObserver) {
			lazyloadObserver.disconnect();
			lazyloadObserver = null;
		}
	} catch (e) {
		console.warn('Failed to cleanup LazyLoad Observer:', e);
	}
	
	try {
		// 清理 Zoomify 实例
		if (zoomifyInstances && zoomifyInstances.length > 0) {
			zoomifyInstances.forEach(function(instance) {
				if (instance && typeof instance.destroy === 'function') {
					instance.destroy();
				}
			});
			zoomifyInstances = [];
		}
	} catch (e) {
		console.warn('Failed to cleanup Zoomify instances:', e);
	}
}

// 初始化错误处理
function initializePageResources() {
	try {
		waterflowInit();
	} catch (e) {
		console.error('waterflowInit failed:', e);
	}
	
	try {
		lazyloadInit();
	} catch (e) {
		console.error('lazyloadInit failed:', e);
	}
	
	// ... 其他初始化
}
```

## 测试策略

### 单元测试

**测试范围：**
- 资源清理函数（`cleanupPjaxResources`）
- LazyLoad 初始化函数（`lazyloadInit`）
- 图片加载函数（`loadImageOptimized`）
- 滚动位置计算逻辑
- GT4 验证码重置逻辑

**测试工具：**
- Jest 或 Mocha（JavaScript 单元测试框架）
- jsdom（模拟 DOM 环境）
- Sinon.js（模拟和监视函数调用）

**测试示例：**

```javascript
describe('cleanupPjaxResources', function() {
	it('should disconnect lazyloadObserver if it exists', function() {
		// 创建模拟 Observer
		lazyloadObserver = {
			disconnect: jest.fn()
		};
		
		cleanupPjaxResources();
		
		expect(lazyloadObserver.disconnect).toHaveBeenCalled();
		expect(lazyloadObserver).toBeNull();
	});
	
	it('should destroy all Zoomify instances', function() {
		// 创建模拟实例
		const mockInstance = {
			destroy: jest.fn()
		};
		zoomifyInstances = [mockInstance];
		
		cleanupPjaxResources();
		
		expect(mockInstance.destroy).toHaveBeenCalled();
		expect(zoomifyInstances).toHaveLength(0);
	});
});

describe('lazyloadInit', function() {
	it('should create new Observer if IntersectionObserver is supported', function() {
		// 模拟 IntersectionObserver
		global.IntersectionObserver = jest.fn(function(callback, options) {
			this.observe = jest.fn();
			this.disconnect = jest.fn();
		});
		
		document.body.innerHTML = '<img class="lazyload" data-src="test.jpg">';
		
		lazyloadInit();
		
		expect(lazyloadObserver).not.toBeNull();
		expect(lazyloadObserver.observe).toHaveBeenCalled();
	});
	
	it('should use fallback if IntersectionObserver is not supported', function() {
		global.IntersectionObserver = undefined;
		
		document.body.innerHTML = '<img class="lazyload" data-src="test.jpg">';
		
		lazyloadInit();
		
		// 验证降级方案被调用
		// ...
	});
});
```

### 属性测试（Property-Based Testing）

属性测试用于验证通用的正确性属性，确保在各种输入下系统行为符合预期。

**测试库：**
- fast-check（JavaScript 属性测试库）

**测试配置：**
- 每个属性测试运行 100 次迭代
- 使用随机生成的输入数据
- 每个测试标注对应的设计属性编号


## 正确性属性

*属性是一个特征或行为，应该在系统的所有有效执行中保持为真——本质上是关于系统应该做什么的形式化陈述。属性作为人类可读规范和机器可验证正确性保证之间的桥梁。*

### 属性 1: PJAX 初始化函数单次调用

*对于任何* PJAX 页面切换过程，关键初始化函数（`waterflowInit`、`lazyloadInit`、`zoomifyInit`）在整个生命周期中应该只被调用一次。

**验证：需求 1.1, 1.2, 1.3**

### 属性 2: LazyLoad Observer 清理完整性

*对于任何* `lazyloadInit()` 调用，如果全局变量 `lazyloadObserver` 已存在，则必须先调用其 `disconnect()` 方法并清空引用，然后再创建新实例。

**验证：需求 2.1, 2.2**

### 属性 3: PJAX beforeReplace 资源清理

*对于任何* PJAX `beforeReplace` 事件触发，系统应该清理 `lazyloadObserver`、`zoomifyInstances` 和 Tippy 实例，并将相关全局变量重置为初始状态。

**验证：需求 2.3, 6.1-6.5, 7.1-7.5**

### 属性 4: IntersectionObserver 降级方案

*对于任何* 不支持 IntersectionObserver 的浏览器环境，`lazyloadInit()` 应该使用滚动监听降级方案，而不是抛出错误或不加载图片。

**验证：需求 2.4**

### 属性 5: 图片加载效果使用 requestAnimationFrame

*对于任何* 图片加载完成事件，应用加载效果（fadeIn 或 slideDown）时应该使用 `requestAnimationFrame` 而非 `setTimeout` 来同步浏览器渲染。

**验证：需求 3.1**

### 属性 6: 图片加载效果 CSS 属性正确性

*对于任何* 图片加载效果，应该正确设置 CSS 属性：fadeIn 效果设置 `opacity` 和 `transition`，slideDown 效果设置 `opacity`、`transform` 和 `transition`。

**验证：需求 3.2, 3.3**

### 属性 7: 图片加载失败处理

*对于任何* 图片加载失败事件，系统应该设置原始 src 并停止重试，不应该进入无限重试循环。

**验证：需求 3.4, 10.4**

### 属性 8: LazyLoad 类名清理

*对于任何* 图片加载完成或失败，系统应该移除 `lazyload` 类和所有 `lazyload-style-*` 类名。

**验证：需求 3.5**

### 属性 9: GT4 验证码状态重置完整性

*对于任何* PJAX `end` 事件且页面包含 `#geetest-captcha` 元素，系统应该重置所有验证码状态变量、清空隐藏字段、清空容器 DOM，并调用初始化函数。

**验证：需求 4.1, 4.2, 4.3, 4.4**

### 属性 10: GT4 验证码初始化错误处理

*对于任何* GT4 验证码初始化失败，系统应该记录警告但不抛出异常，不影响页面其他功能的正常运行。

**验证：需求 4.5**

### 属性 11: 滚动位置恢复正确性

*对于任何* PJAX 页面切换，如果 `pjaxScrollTop` 大于 0，则在 `pjax:complete` 事件中应该恢复到该滚动位置，并在恢复后将 `pjaxScrollTop` 重置为 0。

**验证：需求 5.3, 5.4**

### 属性 12: 分页链接滚动位置计算

*对于任何* 分页链接点击且 Banner 设置为封面模式，系统应该将 `pjaxScrollTop` 设置为内容区域顶部位置减去 80px。

**验证：需求 5.2, 5.5**

### 属性 13: 移动端目录重置调用

*对于任何* PJAX `end` 事件，如果 `window.resetMobileCatalog` 函数存在，系统应该调用该函数（不验证函数内部逻辑）。

**验证：需求 8.1, 8.2**

### 属性 14: 事件监听器清理

*对于任何* PJAX 资源清理过程，系统应该移除所有动态绑定的事件监听器，防止内存泄漏和重复绑定。

**验证：需求 9.2, 9.5**

### 属性 15: 事件监听器防重复绑定

*对于任何* 功能重新初始化，系统应该检查事件监听器是否已存在，避免重复绑定相同的监听器。

**验证：需求 9.3**

## 测试策略（续）

### 属性测试实现

**测试框架：** Jest + fast-check

**测试配置：**
```javascript
// jest.config.js
module.exports = {
	testEnvironment: 'jsdom',
	setupFilesAfterEnv: ['<rootDir>/test/setup.js'],
	collectCoverageFrom: [
		'argontheme.js',
		'!**/node_modules/**',
		'!**/vendor/**'
	],
	coverageThreshold: {
		global: {
			branches: 70,
			functions: 80,
			lines: 80,
			statements: 80
		}
	}
};
```

**属性测试示例：**

```javascript
const fc = require('fast-check');

describe('Property Tests - PJAX LazyLoad Optimization', function() {
	/**
	 * Feature: pjax-lazyload-optimization, Property 1: PJAX 初始化函数单次调用
	 */
	it('should call initialization functions only once during PJAX lifecycle', function() {
		// 创建 spy
		const waterflowSpy = jest.spyOn(window, 'waterflowInit');
		const lazyloadSpy = jest.spyOn(window, 'lazyloadInit');
		const zoomifySpy = jest.spyOn(window, 'zoomifyInit');
		
		// 模拟 PJAX 完整生命周期
		$(document).trigger('pjax:beforeReplace', [[]]);
		$(document).trigger('pjax:complete');
		$(document).trigger('pjax:end');
		
		// 验证每个函数只被调用一次
		expect(waterflowSpy).toHaveBeenCalledTimes(1);
		expect(lazyloadSpy).toHaveBeenCalledTimes(1);
		expect(zoomifySpy).toHaveBeenCalledTimes(1);
		
		// 清理
		waterflowSpy.mockRestore();
		lazyloadSpy.mockRestore();
		zoomifySpy.mockRestore();
	});
	
	/**
	 * Feature: pjax-lazyload-optimization, Property 2: LazyLoad Observer 清理完整性
	 */
	it('should cleanup existing Observer before creating new one', function() {
		// 创建模拟 Observer
		const mockObserver = {
			disconnect: jest.fn(),
			observe: jest.fn()
		};
		window.lazyloadObserver = mockObserver;
		
		// 调用初始化
		lazyloadInit();
		
		// 验证旧 Observer 被清理
		expect(mockObserver.disconnect).toHaveBeenCalled();
		expect(window.lazyloadObserver).not.toBe(mockObserver);
	});
	
	/**
	 * Feature: pjax-lazyload-optimization, Property 3: PJAX beforeReplace 资源清理
	 */
	it('should cleanup all resources on pjax:beforeReplace', function() {
		// 设置资源
		window.lazyloadObserver = {
			disconnect: jest.fn()
		};
		window.zoomifyInstances = [{
			destroy: jest.fn()
		}];
		
		// 触发事件
		$(document).trigger('pjax:beforeReplace', [[]]);
		
		// 验证清理
		expect(window.lazyloadObserver).toBeNull();
		expect(window.zoomifyInstances).toHaveLength(0);
	});
	
	/**
	 * Feature: pjax-lazyload-optimization, Property 5: 图片加载效果使用 requestAnimationFrame
	 */
	it('should use requestAnimationFrame for load effects', function() {
		const rafSpy = jest.spyOn(window, 'requestAnimationFrame');
		
		// 创建测试图片
		document.body.innerHTML = '<img class="lazyload" data-src="test.jpg">';
		const img = document.querySelector('img');
		
		// 调用加载函数
		loadImageOptimized(img, 'fadeIn');
		
		// 模拟图片加载完成
		const tempImg = new Image();
		tempImg.onload();
		
		// 验证 requestAnimationFrame 被调用
		expect(rafSpy).toHaveBeenCalled();
		
		rafSpy.mockRestore();
	});
	
	/**
	 * Feature: pjax-lazyload-optimization, Property 7: 图片加载失败处理
	 */
	it('should handle image load failure without retry', function() {
		document.body.innerHTML = '<img class="lazyload" data-src="invalid.jpg">';
		const img = document.querySelector('img');
		
		// 调用加载函数
		loadImageOptimized(img, 'fadeIn');
		
		// 模拟加载失败
		const tempImg = new Image();
		tempImg.onerror();
		
		// 验证图片 src 被设置且不重试
		expect(img.src).toContain('invalid.jpg');
		expect(img.hasAttribute('data-src')).toBe(false);
		expect(img.classList.contains('lazyload')).toBe(false);
	});
	
	/**
	 * Feature: pjax-lazyload-optimization, Property 11: 滚动位置恢复正确性
	 */
	it('should restore scroll position and reset pjaxScrollTop', function() {
		// 设置滚动位置
		window.pjaxScrollTop = 500;
		
		// 模拟 jQuery scrollTop 方法
		const scrollTopSpy = jest.spyOn($.fn, 'scrollTop');
		
		// 触发 pjax:complete
		$(document).trigger('pjax:complete');
		
		// 验证滚动位置恢复
		expect(scrollTopSpy).toHaveBeenCalledWith(500);
		expect(window.pjaxScrollTop).toBe(0);
		
		scrollTopSpy.mockRestore();
	});
});
```

### 集成测试

**测试场景：**
1. 完整的 PJAX 页面切换流程
2. 多次连续 PJAX 切换
3. 快速点击多个链接
4. 浏览器前进/后退按钮
5. 移动端和桌面端切换

**测试工具：**
- Puppeteer 或 Playwright（浏览器自动化）
- 真实浏览器环境测试

**集成测试示例：**

```javascript
describe('Integration Tests - PJAX LazyLoad', function() {
	let page;
	
	beforeAll(async function() {
		page = await browser.newPage();
		await page.goto('http://localhost:8080');
	});
	
	it('should handle multiple PJAX navigations without memory leak', async function() {
		// 记录初始内存
		const initialMemory = await page.metrics();
		
		// 执行 10 次 PJAX 导航
		for (let i = 0; i < 10; i++) {
			await page.click('.pjax-link');
			await page.waitForSelector('#content');
			await page.goBack();
			await page.waitForSelector('#content');
		}
		
		// 检查内存增长
		const finalMemory = await page.metrics();
		const memoryGrowth = finalMemory.JSHeapUsedSize - initialMemory.JSHeapUsedSize;
		
		// 内存增长应该在合理范围内（< 10MB）
		expect(memoryGrowth).toBeLessThan(10 * 1024 * 1024);
	});
	
	it('should load images lazily after PJAX navigation', async function() {
		await page.click('.pjax-link');
		await page.waitForSelector('#content');
		
		// 检查懒加载图片
		const lazyImages = await page.$$('img.lazyload[data-src]');
		expect(lazyImages.length).toBeGreaterThan(0);
		
		// 滚动到图片位置
		await page.evaluate(function() {
			window.scrollTo(0, document.body.scrollHeight);
		});
		
		// 等待图片加载
		await page.waitForTimeout(1000);
		
		// 验证图片已加载
		const loadedImages = await page.$$('img:not(.lazyload)');
		expect(loadedImages.length).toBeGreaterThan(0);
	});
});
```

### 性能测试

**测试指标：**
- PJAX 页面切换时间
- LazyLoad 初始化时间
- 内存使用情况
- 事件监听器数量

**性能测试示例：**

```javascript
describe('Performance Tests', function() {
	it('should complete PJAX navigation within 500ms', async function() {
		const startTime = performance.now();
		
		await page.click('.pjax-link');
		await page.waitForSelector('#content');
		
		const endTime = performance.now();
		const duration = endTime - startTime;
		
		expect(duration).toBeLessThan(500);
	});
	
	it('should initialize LazyLoad within 100ms', function() {
		const startTime = performance.now();
		
		lazyloadInit();
		
		const endTime = performance.now();
		const duration = endTime - startTime;
		
		expect(duration).toBeLessThan(100);
	});
});
```

### 测试覆盖率目标

- **行覆盖率**：≥ 80%
- **分支覆盖率**：≥ 70%
- **函数覆盖率**：≥ 80%
- **语句覆盖率**：≥ 80%

### 持续集成

**CI 配置（GitHub Actions）：**

```yaml
name: Test PJAX LazyLoad Optimization

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v2
      
      - name: Setup Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '16'
      
      - name: Install dependencies
        run: npm install
      
      - name: Run unit tests
        run: npm test
      
      - name: Run property tests
        run: npm run test:property
      
      - name: Run integration tests
        run: npm run test:integration
      
      - name: Upload coverage
        uses: codecov/codecov-action@v2
        with:
          files: ./coverage/lcov.info
```
