Files
MetonaEditor/e2e/smoke.cjs
T
thzxx cb0caff4cf
CI / test-parser (push) Successful in 9m27s
CI / test-core (push) Successful in 9m34s
CI / test-rest (push) Successful in 9m27s
CI / e2e (push) Failing after 5m15s
CI / verify (20.x) (push) Successful in 9m54s
CI / verify (18.x) (push) Successful in 9m56s
CI / verify (24.x) (push) Successful in 9m48s
feat: v0.4.0 — 安全修复×6 + 增量性能×3 + Playwright E2E + 实例级 i18n
0.3.0 修复版:
- 脚注 id 属性注入 XSS 防护(行内引用 + 脚注区)
- 全局插件注入移至 afterCreate(searchReplace 等依赖 textarea 的插件真正生效)+ use() 同名防重
- 行内渲染缓存附加 refs 指纹,防跨文档引用链接串数据
- 白名单裸标签 <u>/</u> 透传,underline(Ctrl+U)预览可见
- getStatus()/渲染 env 使用实例 locale;replaceAll 替换文本按字面处理
- replaceAllRegex 保留 $1 捕获组语义

0.3.1 性能版:
- 高亮规则按语言缓存(registerLanguage 覆盖失效),bench +33%
- 统计增量计算:字数/词数/行数差异区间,击键零全量扫描
- outline 树构建 O(n²) → 迭代栈 O(n)
- 拖放图片 >500KB 拦截、scrollToLine 真实行高、undo/redo 光标恢复
- selectionChange/cursorMove 与浮动工具栏解耦、unregisterShortcut 大小写归一
- toast 接入 7 种 ANIMATIONS、实例主题订阅随 destroy 断开(dispose)

0.4.0 工程版:
- Playwright 真实浏览器冒烟测试(e2e/,22 项断言)+ CI e2e job
- sideEffects: false 便于 tree-shaking
- MeEditor.destroy() 全面复位(全局钩子清理 + 内部注入钩子重建)
- 实例 locale 统一作用于全部 UI 文案(状态栏/工具栏/右键菜单/大纲)

测试 782 → 826,全绿;typecheck/lint/build 通过
2026-08-09 12:31:14 +08:00

131 lines
5.6 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.
#!/usr/bin/env node
/**
* MetonaEditor 浏览器冒烟测试(Playwright + 本地静态服务)
* 前置: npm run build(需 dist/metona-editor.js 存在)
* 用法: npm run test:e2e
*/
const http = require('http');
const fs = require('fs');
const path = require('path');
const { chromium } = require('playwright');
const ROOT = path.resolve(__dirname, '..');
const MIME = {
'.html': 'text/html; charset=utf-8',
'.js': 'text/javascript',
'.css': 'text/css',
'.svg': 'image/svg+xml',
'.png': 'image/png',
'.mjs': 'text/javascript',
'.cjs': 'text/javascript',
};
const server = http.createServer((req, res) => {
let p = decodeURIComponent((req.url || '/').split('?')[0]);
if (p === '/') p = '/e2e/host.html';
const file = path.resolve(ROOT, '.' + p);
if (!file.startsWith(ROOT) || !fs.existsSync(file) || !fs.statSync(file).isFile()) {
res.writeHead(404); res.end('404 Not Found'); return;
}
res.writeHead(200, { 'Content-Type': MIME[path.extname(file)] || 'text/plain' });
fs.createReadStream(file).pipe(res);
});
let passed = 0;
const check = (name, cond) => {
if (!cond) throw new Error(`FAIL: ${name}`);
passed++;
console.log(` ✓ ${name}`);
};
(async () => {
await new Promise((r) => server.listen(0, '127.0.0.1', r));
const port = server.address().port;
console.log(`MetonaEditor E2E · http://127.0.0.1:${port}/`);
const browser = await chromium.launch();
const page = await browser.newPage();
const errors = [];
page.on('console', (m) => { if (m.type() === 'error') errors.push(m.text()); });
page.on('pageerror', (e) => errors.push(String(e)));
await page.goto(`http://127.0.0.1:${port}/`, { waitUntil: 'load' });
// 1. 编辑器渲染
await page.waitForSelector('.me-wrapper', { timeout: 10000 });
check('编辑器 DOM 渲染', await page.locator('.me-wrapper').count() === 1);
check('textarea 存在', await page.locator('.me-textarea').count() === 1);
check('初始 value 写入', (await page.locator('.me-textarea').inputValue()).includes('# E2E Title'));
// 2. 预览渲染
const h1 = await page.locator('.me-preview h1').textContent();
check('预览渲染 h1', h1 === 'E2E Title');
check('预览渲染粗体', (await page.locator('.me-preview strong').count()) === 1);
check('预览渲染下划线 <u>', (await page.locator('.me-preview u').count()) === 1);
check('预览渲染表格', (await page.locator('.me-preview table').count()) === 1);
// 3. 语法高亮
check('代码块高亮 keyword', (await page.locator('.me-preview .me-hl-keyword').count()) > 0);
check('代码块高亮 comment', (await page.locator('.me-preview .me-hl-comment').count()) > 0);
// 4. 输入 → 预览实时更新
await page.locator('.me-textarea').fill('# 输入测试\n\nhello **world**');
await page.waitForTimeout(100);
check('输入后预览更新 h1', (await page.locator('.me-preview h1').textContent()) === '输入测试');
check('输入后预览更新 strong', (await page.locator('.me-preview strong').textContent()) === 'world');
// 5. 工具栏命令(bold 包裹选区)
await page.locator('.me-textarea').fill('select me');
await page.locator('.me-textarea').evaluate((el) => { el.focus(); el.setSelectionRange(0, 9); });
await page.locator('.me-btn[data-action="bold"]').click();
check('工具栏 bold 命令', (await page.locator('.me-textarea').inputValue()) === '**select me**');
// 6. 模式切换
await page.locator('.me-btn[data-mode="preview"]').click();
check('切换到 preview 模式', await page.locator('.me-body.me-mode-preview').count() === 1);
await page.locator('.me-btn[data-mode="edit"]').click();
check('切换到 edit 模式', await page.locator('.me-body.me-mode-edit').count() === 1);
await page.locator('.me-btn[data-mode="split"]').click();
check('切换到 split 模式', await page.locator('.me-body.me-mode-split').count() === 1);
// 7. 主题切换
await page.evaluate(() => window.__ed.setTheme('dark'));
check('实例主题 dark', (await page.locator('.me-wrapper').getAttribute('data-md-theme')) === 'dark');
// 8. 搜索插件(Ctrl+F
await page.locator('.me-textarea').press('Control+f');
check('Ctrl+F 打开搜索面板', (await page.locator('.me-search').count()) === 1);
await page.locator('.me-search-find').fill('select');
await page.locator('.me-search-next').click();
check('搜索面板查找选中', (await page.locator('.me-textarea').evaluate((el) => el.value.substring(el.selectionStart, el.selectionEnd))) === 'select');
await page.keyboard.press('Escape');
// 9. 统计状态栏
check('状态栏字数统计', (await page.locator('.me-statusbar').textContent()).includes('字符'));
// 10. 撤销(bold 后撤销回到初始内容)
await page.locator('.me-textarea').press('Control+z');
await page.waitForTimeout(50);
check('Ctrl+Z 撤销', (await page.locator('.me-textarea').inputValue()).includes('E2E Title'));
// 11. 实例 API 直调
const stats = await page.evaluate(() => window.__ed.getStats());
check('getStats 返回结构', typeof stats.characters === 'number' && typeof stats.words === 'number');
const status = await page.evaluate(() => window.__ed.getStatus());
check('getStatus 返回结构', status.mode === 'split' && status.theme === 'dark' && status.locale === 'zh-CN');
await browser.close();
server.close();
if (errors.length) {
console.error('\n页面错误:', errors);
process.exit(1);
}
console.log(`\n全部通过: ${passed} 项断言 · 无页面错误`);
})().catch((e) => {
console.error('\n' + (e && e.message ? e.message : e));
try { server.close(); } catch (_) {}
process.exit(1);
});