paper-burner/tests/MODULE_FIX_RECOMMENDATIONS.md

26 KiB
Raw Permalink Blame History

模块修复建议清单

🔴 高优先级 - 必须修复

1. TextFittingAdapter - 未定义的常量

问题位置: TextFittingAdapter.js line 71

当前代码:

preprocessGlobalFontSizes(contentListJson, translatedContentList) {
  // ...
  contentListJson.forEach((item, idx) => {
    // ...
    const height = (bbox[3] - bbox[1]) / BBOX_NORMALIZED_RANGE;  // ❌ 未定义!
  });
}

修复方案:

preprocessGlobalFontSizes(contentListJson, translatedContentList) {
  if (this.hasPreprocessed) return;

  console.log('[TextFittingAdapter] 开始预处理全局字号...');
  const startTime = performance.now();

  const globalFontScale = this.options.globalFontScale;
  const BBOX_NORMALIZED_RANGE = 1000;  // ✅ 添加这行

  contentListJson.forEach((item, idx) => {
    if (item.type !== 'text' || !item.bbox) return;

    const translatedItem = translatedContentList[idx];
    if (!translatedItem || !translatedItem.text) return;

    const bbox = item.bbox;
    const height = (bbox[3] - bbox[1]) / BBOX_NORMALIZED_RANGE;

    const estimatedFontSize = height * globalFontScale;

    this.globalFontSizeCache.set(idx, {
      estimatedFontSize: estimatedFontSize,
      bbox: bbox
    });
  });

  console.log(`[TextFittingAdapter] 预处理完成:全局缩放=${globalFontScale}, 耗时=${(performance.now() - startTime).toFixed(0)}ms`);
  this.hasPreprocessed = true;
}

影响: 🔴 严重 - 会导致运行时NaN错误


2. PDFExporter - Canvas和PDF文本高度公式不一致

问题位置:

  • Canvas版本: history_pdf_compare.js line 1463-1465
  • PDF版本: PDFExporter.js line 272-274

当前代码对比:

Canvas (错误):

const totalHeight = lines.length === 1
  ? mid * 1.2  // 单行文本额外增加20%
  : (lines.length - 1) * lineHeight + mid * 1.2;

PDF (错误):

const totalHeight = lines.length > 0
  ? (lines.length - 1) * lineHeight + mid  // 单行文本没有增加
  : 0;

修复方案 - 统一为一致的公式:

// 在 PDFExporter.calculatePdfTextLayout() 中修复 (line 272-274)
// 改为与 Canvas 版本一致:

const totalHeight = lines.length > 0
  ? (lines.length - 1) * lineHeight + mid * 1.2  // ✅ 与Canvas统一
  : 0;

// 同时在 drawPlainTextWithFitting() 中保持一致 (line 1463-1465)
const totalHeight = lines.length > 0
  ? (lines.length - 1) * lineHeight + mid * 1.2  // ✅ 保持一致
  : 0;

说明:

  • 这个 * 1.2 是为了给文本留出额外的垂直空间
  • Canvas 中确实使用了这个系数
  • PDF 版本遗漏了导致文本可能超出bbox

影响: 🔴 严重 - PDF导出的文本大小会与Canvas显示不同


3. SegmentManager - 事件监听器无法清理

问题位置: SegmentManager.js lines 216-234, 397-413

当前代码:

initLazyLoadingSegments() {
  if (!this._lazyInitialized) {
    this.originalScroll.addEventListener('scroll', () => onScroll(this.originalScroll));
    this.translationScroll.addEventListener('scroll', () => onScroll(this.translationScroll));
    // ❌ 这些匿名箭头函数无法被移除
    this._lazyInitialized = true;
  }
}

destroy() {
  // 注意:由于事件监听使用了箭头函数,无法直接移除
  // 这里设置标记位,防止继续渲染
  // ❌ 这不能真正清理资源!
  this.segments = [];
  this.pageInfos = [];
}

修复方案:

constructor(pdfDoc, options = {}) {
  // ... 其他初始化 ...

  this._destroyed = false;  // ✅ 添加销毁标志
  this._scrollHandler = null;  // ✅ 保存事件处理函数引用
}

initLazyLoadingSegments() {
  if (!this.originalScroll || !this.translationScroll) return;

  // 初始渲染可见段
  this.renderVisibleSegments(this.originalScroll);

  const onScroll = (scroller) => {
    clearTimeout(this._lazyScrollTimer);
    this._lazyScrollTimer = setTimeout(() => {
      if (!this._destroyed) {  // ✅ 检查销毁标志
        this.renderVisibleSegments(scroller);
      }
    }, this.options.scrollDebounceMs);
  };

  if (!this._lazyInitialized) {
    // ✅ 保存事件处理函数引用以便后续移除
    this._scrollHandler = onScroll;

    const originalScrollHandler = () => onScroll(this.originalScroll);
    const translationScrollHandler = () => onScroll(this.translationScroll);

    // ✅ 保存处理函数引用
    this._originalScrollHandler = originalScrollHandler;
    this._translationScrollHandler = translationScrollHandler;

    this.originalScroll.addEventListener('scroll', originalScrollHandler);
    this.translationScroll.addEventListener('scroll', translationScrollHandler);
    this._lazyInitialized = true;
  }
}

destroy() {
  this._destroyed = true;  // ✅ 设置销毁标志

  // ✅ 正确移除事件监听器
  if (this._lazyInitialized && this.originalScroll && this.translationScroll) {
    if (this._originalScrollHandler) {
      this.originalScroll.removeEventListener('scroll', this._originalScrollHandler);
    }
    if (this._translationScrollHandler) {
      this.translationScroll.removeEventListener('scroll', this._translationScrollHandler);
    }
  }

  // ✅ 清除定时器
  if (this._lazyScrollTimer) {
    clearTimeout(this._lazyScrollTimer);
    this._lazyScrollTimer = null;
  }

  // ✅ 清空 DOM
  if (this.originalSegmentsContainer) {
    this.originalSegmentsContainer.innerHTML = '';
  }
  if (this.translationSegmentsContainer) {
    this.translationSegmentsContainer.innerHTML = '';
  }

  this.segments = [];
  this.pageInfos = [];
}

影响: 🔴 严重 - 内存泄漏,事件处理器持续执行


🟡 中优先级 - 应该改进

4. TextFittingAdapter - 缺少错误处理改进

问题位置: TextFittingAdapter.js line 34-57

当前代码:

initialize() {
  if (typeof TextFittingEngine === 'undefined') {
    console.error('[TextFittingAdapter] TextFittingEngine 未加载!...');
    return;  // ❌ 静默失败
  }
  // ...
}

修复方案:

initialize() {
  // ✅ 改为throw更容易被发现
  if (typeof TextFittingEngine === 'undefined') {
    throw new Error('[TextFittingAdapter] TextFittingEngine 未加载!请确保 js/utils/text-fitting.js 已正确引入');
  }

  try {
    this.textFittingEngine = new TextFittingEngine({
      initialScale: this.options.initialScale,
      minScale: this.options.minScale,
      scaleStepHigh: this.options.scaleStepHigh,
      scaleStepLow: this.options.scaleStepLow,
      lineSkipCJK: this.options.lineSkipCJK,
      lineSkipWestern: this.options.lineSkipWestern,
      minLineHeight: this.options.minLineHeight
    });

    console.log('[TextFittingAdapter] 文本自适应引擎已启用');
  } catch (error) {
    console.error('[TextFittingAdapter] 文本自适应引擎初始化失败:', error);
    throw error;  // ✅ 将错误传播给调用者
  }
}

使用方式:

try {
  const textFitter = new TextFittingAdapter();
  textFitter.initialize();
} catch (error) {
  console.error('初始化失败,将使用回退方案');
  // 处理回退...
}

影响: 🟡 中等 - 便于发现问题


5. TextFittingAdapter - 添加参数验证

问题位置: TextFittingAdapter.js line 64-93

当前代码:

preprocessGlobalFontSizes(contentListJson, translatedContentList) {
  // ❌ 没有验证参数
  if (this.hasPreprocessed) return;

  const globalFontScale = this.options.globalFontScale;

  contentListJson.forEach((item, idx) => {  // ❌ 可能不是数组
    // ...
  });
}

修复方案:

preprocessGlobalFontSizes(contentListJson, translatedContentList) {
  if (this.hasPreprocessed) return;

  // ✅ 添加参数验证
  if (!contentListJson || !Array.isArray(contentListJson)) {
    console.warn('[TextFittingAdapter] 无效的 contentListJson跳过预处理');
    return;
  }

  if (!translatedContentList || !Array.isArray(translatedContentList)) {
    console.warn('[TextFittingAdapter] 无效的 translatedContentList跳过预处理');
    return;
  }

  console.log('[TextFittingAdapter] 开始预处理全局字号...');
  const startTime = performance.now();

  const globalFontScale = this.options.globalFontScale;
  const BBOX_NORMALIZED_RANGE = 1000;

  contentListJson.forEach((item, idx) => {
    if (item.type !== 'text' || !item.bbox) return;

    const translatedItem = translatedContentList[idx];
    if (!translatedItem || !translatedItem.text) return;

    const bbox = item.bbox;
    const height = (bbox[3] - bbox[1]) / BBOX_NORMALIZED_RANGE;

    const estimatedFontSize = height * globalFontScale;

    this.globalFontSizeCache.set(idx, {
      estimatedFontSize: estimatedFontSize,
      bbox: bbox
    });
  });

  console.log(`[TextFittingAdapter] 预处理完成:全局缩放=${globalFontScale}, 耗时=${(performance.now() - startTime).toFixed(0)}ms`);
  this.hasPreprocessed = true;
}

影响: 🟡 中等 - 防止崩溃


6. TextFittingAdapter - 添加ctx验证

问题位置: TextFittingAdapter.js line 309

当前代码:

wrapText(ctx, text, maxWidth) {
  if (!text) return [];

  const lines = [];
  let currentLine = '';

  // ...
  const metrics = ctx.measureText(testLine);  // ❌ ctx 可能无效
}

修复方案:

wrapText(ctx, text, maxWidth) {
  // ✅ 添加验证
  if (!text) return [];

  if (!ctx || typeof ctx.measureText !== 'function') {
    console.warn('[TextFittingAdapter] 无效的 canvas context');
    // 返回简单分割
    return text.split('\n').length > 0 ? text.split('\n') : [''];
  }

  if (typeof maxWidth !== 'number' || maxWidth <= 0) {
    console.warn('[TextFittingAdapter] 无效的 maxWidth');
    return text.split('\n');
  }

  const lines = [];
  let currentLine = '';

  const segments = text.split(/([。?!,、;:\n])/);

  for (let segment of segments) {
    if (!segment) continue;

    if (/^[。?!,、;:]$/.test(segment)) {
      currentLine += segment;
      continue;
    }

    if (segment === '\n') {
      if (currentLine) {
        lines.push(currentLine);
        currentLine = '';
      }
      continue;
    }

    for (let i = 0; i < segment.length; i++) {
      const char = segment[i];
      const testLine = currentLine + char;
      const metrics = ctx.measureText(testLine);

      if (metrics.width > maxWidth && currentLine.length > 0) {
        lines.push(currentLine);
        currentLine = char;
      } else {
        currentLine = testLine;
      }
    }
  }

  if (currentLine) {
    lines.push(currentLine);
  }

  return lines.length > 0 ? lines : [''];
}

影响: 🟡 中等 - 防止崩溃


7. PDFExporter - 改进fontkit失败处理

问题位置: PDFExporter.js line 73-90, 395-410

当前代码:

let font = null;
try {
  if (typeof fontkit === 'undefined') {
    throw new Error('fontkit 未加载,无法嵌入中文字体');
  }
  // ...
  font = await pdfDoc.embedFont(fontBytes);
} catch (fontError) {
  console.error('[PDFExporter] 中文字体加载失败:', fontError);
  if (showNotification) {
    showNotification('中文字体加载失败无法导出PDF: ' + fontError.message, 'error');
  }
  throw fontError;  // ❌ 中断流程
}

// 但下面又有:
if (typeof fontkit === 'undefined') {
  await new Promise((resolve, reject) => {
    // ...
    script.onerror = (error) => {
      console.warn('[PDFExporter] fontkit 加载失败:', error);
      resolve();  // ❌ 失败也继续!
    };
  });
}

修复方案:

async loadPdfLib() {
  // 加载 pdf-lib
  if (typeof PDFLib === 'undefined') {
    await new Promise((resolve, reject) => {
      const script = document.createElement('script');
      script.src = this.options.pdfLibUrl;
      script.onload = () => {
        console.log('[PDFExporter] pdf-lib 加载成功');
        this.pdfLibLoaded = true;
        resolve();
      };
      script.onerror = (error) => {
        console.error('[PDFExporter] pdf-lib 加载失败:', error);
        reject(new Error('Failed to load pdf-lib library'));
      };
      document.head.appendChild(script);
    });
  }

  // 加载 fontkit (可选,失败不中断)
  if (typeof fontkit === 'undefined') {
    await new Promise((resolve, reject) => {
      const script = document.createElement('script');
      script.src = this.options.fontkitUrl;
      script.onload = () => {
        console.log('[PDFExporter] fontkit 加载成功');
        this.fontkitLoaded = true;
        resolve();
      };
      script.onerror = (error) => {
        console.warn('[PDFExporter] fontkit 加载失败,中文字体可能无法正确显示:', error);
        this.fontkitLoaded = false;
        resolve();  // 不中断流程,但记录失败
      };
      document.head.appendChild(script);
    });
  }
}

async exportStructuredTranslation(originalPdfBase64, translatedContentList, showNotification = null) {
  try {
    // ... 前面的检查 ...

    // ✅ 改进字体加载
    let font = null;
    if (!this.fontkitLoaded) {
      console.warn('[PDFExporter] fontkit 未成功加载,中文字体可能无法正确显示');
      if (showNotification) {
        showNotification('警告:中文字体可能无法正确显示', 'warning');
      }
    } else {
      try {
        console.log('[PDFExporter] 正在加载中文字体...');
        const fontBytes = await fetch(this.options.fontUrl).then(res => {
          if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
          return res.arrayBuffer();
        });

        font = await pdfDoc.embedFont(fontBytes);
        console.log('[PDFExporter] 中文字体加载成功');
      } catch (fontError) {
        console.error('[PDFExporter] 中文字体加载失败:', fontError);
        if (showNotification) {
          showNotification('警告:中文字体加载失败,将使用默认字体', 'warning');
        }
        // ✅ 不中断流程,继续使用默认字体
      }
    }

    // 如果没有字体,使用默认字体
    if (!font) {
      console.warn('[PDFExporter] 使用PDF默认字体中文可能显示为空');
      // 可以选择使用内置字体或继续
    }

    // ... 后续处理 ...
  } catch (error) {
    // ...
  }
}

影响: 🟡 中等 - 提高鲁棒性


8. PDFExporter - 添加showNotification类型检查

问题位置: PDFExporter.js 多处

当前代码:

if (showNotification) {
  showNotification('没有翻译内容可导出', 'warning');  // ❌ 未检查是否为函数
}

修复方案:

// 在类中添加辅助方法
_notify(message, type = 'info') {
  if (typeof this._showNotification === 'function') {
    this._showNotification(message, type);
  } else if (!this._notificationDisabled) {
    console.log(`[${type.toUpperCase()}] ${message}`);
  }
}

constructor(options = {}, showNotification = null) {
  this.options = Object.assign({
    fontUrl: 'https://...',
    pdfLibUrl: 'https://...',
    fontkitUrl: 'https://...',
    bboxNormalizedRange: 1000
  }, options);

  this._showNotification = typeof showNotification === 'function' ? showNotification : null;
  this.pdfLibLoaded = false;
  this.fontkitLoaded = false;
}

async exportStructuredTranslation(originalPdfBase64, translatedContentList, showNotification = null) {
  // ✅ 更新 showNotification
  if (typeof showNotification === 'function') {
    this._showNotification = showNotification;
  }

  try {
    if (!translatedContentList || translatedContentList.length === 0) {
      this._notify('没有翻译内容可导出', 'warning');
      return;
    }

    if (!originalPdfBase64) {
      this._notify('原始PDF数据不可用', 'error');
      return;
    }

    this._notify('正在生成译文PDF请稍候...', 'info');

    // ... 后续代码 ...
  } catch (error) {
    this._notify('导出失败: ' + error.message, 'error');
  }
}

影响: 🟡 中等 - 提高健壮性


9. SegmentManager - 改进离屏canvas重用

问题位置: SegmentManager.js line 280-299

当前代码:

async renderSegment(seg) {
  const off = document.createElement('canvas');  // ❌ 每次创建
  const offCtx = off.getContext('2d', { willReadFrequently: true, alpha: false });

  for (const p of seg.pages) {
    if (off.width !== p.width) off.width = p.width;  // ❌ 频繁重新分配
    if (off.height !== p.height) off.height = p.height;
    // ...
  }
  // ❌ canvas 没有被清理,垃圾回收等待
}

修复方案:

constructor(pdfDoc, options = {}) {
  // ... 其他初始化 ...
  this._offscreenCanvas = null;  // ✅ 缓存离屏canvas
  this._offscreenCtx = null;      // ✅ 缓存context
  this._maxOffscreenSize = { width: 0, height: 0 };  // ✅ 追踪最大尺寸
}

_getOffscreenCanvas(width, height) {
  // ✅ 复用或创建离屏canvas
  if (!this._offscreenCanvas) {
    this._offscreenCanvas = document.createElement('canvas');
    this._offscreenCtx = this._offscreenCanvas.getContext('2d', {
      willReadFrequently: true,
      alpha: false
    });
  }

  // ✅ 只在需要时扩大(不缩小,避免频繁分配)
  if (width > this._maxOffscreenSize.width || height > this._maxOffscreenSize.height) {
    this._offscreenCanvas.width = Math.max(width, this._maxOffscreenSize.width);
    this._offscreenCanvas.height = Math.max(height, this._maxOffscreenSize.height);
    this._maxOffscreenSize.width = this._offscreenCanvas.width;
    this._maxOffscreenSize.height = this._offscreenCanvas.height;
    console.log(`[SegmentManager] 离屏canvas扩展为 ${this._offscreenCanvas.width}x${this._offscreenCanvas.height}`);
  }

  return { canvas: this._offscreenCanvas, ctx: this._offscreenCtx };
}

async renderSegment(seg) {
  for (const p of seg.pages) {
    const { canvas: off, ctx: offCtx } = this._getOffscreenCanvas(p.width, p.height);

    // ✅ 重新设置尺寸为当前page的尺寸只是清除不重新分配
    off.width = p.width;
    off.height = p.height;

    offCtx.clearRect(0, 0, off.width, off.height);
    await p.page.render({ canvasContext: offCtx, viewport: p.viewport }).promise;

    // 绘制到左右段画布
    seg.left.ctx.drawImage(off, 0, p.yInSegPx);
    seg.right.ctx.drawImage(off, 0, p.yInSegPx);
  }

  // 绘制 overlays
  await this.renderSegmentOverlays(seg);
}

destroy() {
  // ... 其他清理 ...

  // ✅ 清理离屏canvas
  this._offscreenCanvas = null;
  this._offscreenCtx = null;
  this._maxOffscreenSize = { width: 0, height: 0 };

  this.segments = [];
  this.pageInfos = [];
}

影响: 🟡 中等 - 性能优化


10. SegmentManager - 添加容器验证

问题位置: SegmentManager.js line 209-210

当前代码:

createSegmentDom(seg, dpr) {
  // ...
  buildSide(this.originalSegmentsContainer, 'left');  // ❌ 容器可能为null
  buildSide(this.translationSegmentsContainer, 'right');
}

const buildSide = (container, side) => {
  // ...
  container.appendChild(wrapper);  // ❌ 如果container为null会崩溃
};

修复方案:

createSegmentDom(seg, dpr) {
  // ✅ 验证容器
  if (!this.originalSegmentsContainer || !this.translationSegmentsContainer) {
    console.error('[SegmentManager] 容器未初始化无法创建段DOM');
    return false;
  }

  const cssWidth = seg.widthPx / dpr;
  const cssHeight = seg.heightPx / dpr;

  const buildSide = (container, side) => {
    if (!container) {
      console.error(`[SegmentManager] ${side} 容器为null`);
      return;
    }

    const wrapper = document.createElement('div');
    wrapper.className = 'pdf-segment-wrapper';
    wrapper.style.position = 'relative';
    wrapper.style.display = 'block';
    wrapper.style.width = cssWidth + 'px';
    wrapper.style.height = cssHeight + 'px';
    wrapper.style.margin = '0';

    const canvas = document.createElement('canvas');
    canvas.width = seg.widthPx;
    canvas.height = seg.heightPx;
    canvas.style.width = cssWidth + 'px';
    canvas.style.height = cssHeight + 'px';
    const ctx = canvas.getContext('2d', { willReadFrequently: true, alpha: false });

    const overlay = document.createElement('canvas');
    overlay.width = seg.widthPx;
    overlay.height = seg.heightPx;
    overlay.style.width = cssWidth + 'px';
    overlay.style.height = cssHeight + 'px';
    overlay.style.position = 'absolute';
    overlay.style.left = '0';
    overlay.style.top = '0';
    const overlayCtx = overlay.getContext('2d', { willReadFrequently: true });

    wrapper.appendChild(canvas);
    wrapper.appendChild(overlay);
    container.appendChild(wrapper);

    const sideObj = { wrapper, canvas, ctx, overlay, overlayCtx };
    if (side === 'left') seg.left = sideObj;
    else seg.right = sideObj;

    // 绑定点击事件
    if (side === 'left' && this.onOverlayClick) {
      overlay.addEventListener('click', (e) => this.onOverlayClick(e, seg));
    }
  };

  buildSide(this.originalSegmentsContainer, 'left');
  buildSide(this.translationSegmentsContainer, 'right');

  return true;
}

影响: 🟡 中等 - 防止崩溃


🟢 低优先级 - 可选改进

11. SegmentManager - 添加BBOX_NORMALIZED_RANGE验证

问题位置: SegmentManager.js line 334-365

当前代码:

const BBOX_NORMALIZED_RANGE = this.options.bboxNormalizedRange;
// ...
const scaleX = p.width / BBOX_NORMALIZED_RANGE;  // ❌ 如果为0会导致Infinity

修复方案:

async clearTextInSegment(seg) {
  if (!this.contentListJson || !this.clearTextInBbox) {
    console.warn('[SegmentManager] 缺少清除文字依赖');
    return;
  }

  const BBOX_NORMALIZED_RANGE = this.options.bboxNormalizedRange;

  // ✅ 添加验证
  if (!BBOX_NORMALIZED_RANGE || BBOX_NORMALIZED_RANGE <= 0) {
    console.error('[SegmentManager] 无效的 bboxNormalizedRange:', BBOX_NORMALIZED_RANGE);
    return;
  }

  // ... 后续代码 ...
}

影响: 🟢 低 - 防止数值错误


12. PDFExporter - 改进rgb色值处理

问题位置: PDFExporter.js line 133

当前代码:

page.drawRectangle({
  x: x,
  y: y,
  width: width,
  height: height,
  color: rgb(1, 1, 1),  // ⚠️ 这在pdf-lib中是正确的0-1范围
});

说明: 实际上这是正确的pdf-lib使用0-1范围的RGB值。但建议添加注释

page.drawRectangle({
  x: x,
  y: y,
  width: width,
  height: height,
  color: rgb(1, 1, 1),  // ✅ pdf-lib使用0-1范围不是0-255
});

影响: 🟢 低 - 文档优化


修复优先级排序

Phase 1 - 立即修复 (必须在测试前)

  1. TextFittingAdapter - 添加 BBOX_NORMALIZED_RANGE 定义
  2. PDFExporter - 统一Canvas和PDF文本高度公式
  3. SegmentManager - 修复事件监听器清理

Phase 2 - 尽快修复 (本周内)

  1. TextFittingAdapter - 改进错误处理
  2. TextFittingAdapter - 添加参数验证
  3. TextFittingAdapter - 添加ctx验证
  4. PDFExporter - 改进fontkit失败处理
  5. PDFExporter - 添加showNotification类型检查

Phase 3 - 后续优化 (下周)

  1. SegmentManager - 改进离屏canvas重用
  2. SegmentManager - 添加容器验证
  3. SegmentManager - 添加BBOX_NORMALIZED_RANGE验证
  4. PDFExporter - 改进rgb色值注释

测试检查清单

修复完成后,按以下顺序测试:

  • TextFittingAdapter.initialize() 失败时是否正确抛出错误
  • preprocessGlobalFontSizes() 接收无效参数时是否正确处理
  • wrapText() 接收无效ctx时是否降级处理
  • PDFExporter 导出的PDF文本大小是否与Canvas显示一致
  • PDFExporter fontkit加载失败时是否继续导出可能带警告
  • SegmentManager 滚动时是否继续渲染,销毁后是否停止
  • SegmentManager destroy() 调用后内存是否释放
  • SegmentManager setContainers(null, ...) 时是否正确处理
  • 长PDF (100+页) 是否能正确分段和渲染
  • 切换PDF后旧的事件监听器是否被清理

集成测试示例

// 完整的集成测试
async function testModuleIntegration() {
  console.log('开始模块集成测试...');

  // 1. 测试TextFittingAdapter
  console.log('\n1. 测试 TextFittingAdapter');
  try {
    const textFitter = new TextFittingAdapter({
      globalFontScale: 0.9
    });

    // 应该throw而不是静默失败
    try {
      textFitter.initialize();
      console.warn('⚠️ initialize() 应该检查TextFittingEngine');
    } catch (e) {
      console.log('✅ initialize() 正确抛出错误');
    }

    // 测试参数验证
    textFitter.preprocessGlobalFontSizes(null, null);
    console.log('✅ preprocessGlobalFontSizes() 接受无效参数');

    // 测试wrapText验证
    const canvas = document.createElement('canvas');
    const ctx = canvas.getContext('2d');
    const lines = textFitter.wrapText(null, 'test', 100);  // 应该降级
    console.log('✅ wrapText() 接受无效ctx返回:', lines);

  } catch (error) {
    console.error('❌ TextFittingAdapter测试失败:', error);
  }

  // 2. 测试PDFExporter
  console.log('\n2. 测试 PDFExporter');
  try {
    const exporter = new PDFExporter();

    // 测试没有翻译数据
    await exporter.exportStructuredTranslation('', [], (msg, type) => {
      console.log(`[${type}] ${msg}`);
    });
    console.log('✅ 空翻译数据处理正确');

  } catch (error) {
    console.error('❌ PDFExporter测试失败:', error);
  }

  // 3. 测试SegmentManager
  console.log('\n3. 测试 SegmentManager');
  try {
    // 模拟pdfDoc
    const mockPdfDoc = {
      numPages: 10,
      getPage: async (n) => ({
        getViewport: ({ scale }) => ({ width: 612, height: 792, scale }),
        render: async ({ canvasContext, viewport }) => ({ promise: Promise.resolve() })
      })
    };

    const manager = new SegmentManager(mockPdfDoc);

    // 设置依赖和容器
    const origContainer = document.createElement('div');
    const transContainer = document.createElement('div');

    manager.setContainers(origContainer, transContainer, window, window);

    // 测试setContainers(null)
    manager.setContainers(null, null, null, null);
    console.log('✅ setContainers() 接受nullcreateSegmentDom应该降级');

    // 测试destroy
    manager.destroy();
    console.log('✅ destroy() 执行成功');

  } catch (error) {
    console.error('❌ SegmentManager测试失败:', error);
  }

  console.log('\n✅ 模块集成测试完成');
}

// 运行测试
testModuleIntegration();