'use strict'; /* ⑦ 终端 tokenizer 与 cp/mv/rm/touch 语义 */ module.exports = { name: '05-terminal', title: '终端引号/转义、cp/mv 目标语义、rm -r、touch mtime、错误不重复', async run({ t, track, newPage, unlock, http }) { const env = track(await newPage(http)); const { page } = env; await unlock(page); const H = '/Users/guest'; await page.evaluate(() => window.Apps.open('terminal')); await page.waitForTimeout(300); const type = cmd => page.evaluate(c => { const win = window.WM.windowsForApp('terminal')[0]; const input = win.body.querySelector('.term-input'); input.value = c; input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true, cancelable: true })); }, cmd); const lines = () => page.evaluate(() => [...window.WM.windowsForApp('terminal')[0].body.querySelectorAll('.term-line')].map(n => n.textContent)); const errLines = () => page.evaluate(() => [...window.WM.windowsForApp('terminal')[0].body.querySelectorAll('.term-line.term-err')].map(n => n.textContent)); const exists = p => page.evaluate(pp => window.FS.exists(pp), p); const isDir = p => page.evaluate(pp => window.FS.isDir(pp), p); // ---- tokenizer:引号 ---- await type(`mkdir '${H}/Desktop/Test Dir'`); await page.waitForTimeout(120); t.ok(await isDir(`${H}/Desktop/Test Dir`), `mkdir 'Test Dir' 单引号生效`); await type(`cd "${H}/Desktop/Test Dir"`); await page.waitForTimeout(120); let prompt = await page.evaluate(() => window.WM.windowsForApp('terminal')[0].appState.cwd); t.eq(prompt, `${H}/Desktop/Test Dir`, 'cd "…/Test Dir" 双引号生效'); await type(`cd ~/Desktop/'Test Dir'`); await page.waitForTimeout(120); prompt = await page.evaluate(() => window.WM.windowsForApp('terminal')[0].appState.cwd); t.eq(prompt, `${H}/Desktop/Test Dir`, `cd ~/Desktop/'Test Dir' 拼接引号生效`); await type(`cd ${H.replace(/\//g, '\\/')}/Desktop/Test\\ Dir`.replace('\\/', '/')); // 反斜杠转义空格 await page.waitForTimeout(120); prompt = await page.evaluate(() => window.WM.windowsForApp('terminal')[0].appState.cwd); t.eq(prompt, `${H}/Desktop/Test Dir`, '反斜杠转义空格生效'); // ---- touch 更新 mtime ---- await type('touch a.txt'); await page.waitForTimeout(120); const m1 = await page.evaluate(p => window.FS.node(p).mtime, `${H}/Desktop/Test Dir/a.txt`); await page.waitForTimeout(30); await type('touch a.txt'); await page.waitForTimeout(120); const m2 = await page.evaluate(p => window.FS.node(p).mtime, `${H}/Desktop/Test Dir/a.txt`); t.ok(m2 > m1, `touch 已存在文件更新 mtime(${m1} → ${m2})`); // ---- cp / mv 目标文件名语义 ---- await type('cp a.txt b.txt'); // 目标文件路径:改名复制 await page.waitForTimeout(120); t.ok(await exists(`${H}/Desktop/Test Dir/b.txt`), 'cp a.txt b.txt 生成 b.txt'); let errs = await errLines(); t.eq(errs.filter(e => e.includes('cp')).length, 0, 'cp 成功无错误输出'); await type(`mkdir sub`); await type('cp a.txt sub'); // 目标目录:保留原名 await page.waitForTimeout(120); t.ok(await exists(`${H}/Desktop/Test Dir/sub/a.txt`), 'cp 到目录保留原名'); await type('mv b.txt c.txt'); // 目标文件路径:改名移动 await page.waitForTimeout(120); t.ok(await exists(`${H}/Desktop/Test Dir/c.txt`), 'mv b.txt c.txt 生成 c.txt'); t.eq(await exists(`${H}/Desktop/Test Dir/b.txt`), false, 'mv 后源消失'); errs = await errLines(); t.eq(errs.filter(e => e.includes('mv')).length, 0, 'mv 改名成功且无错误输出(只移动一次)'); await type('mv c.txt sub'); // 目标目录 await page.waitForTimeout(120); t.ok(await exists(`${H}/Desktop/Test Dir/sub/c.txt`), 'mv 到目录保留原名'); // ---- rm 语义 ---- await type('rm sub'); // 目录不带 -r → 报错 await page.waitForTimeout(120); t.ok(await isDir(`${H}/Desktop/Test Dir/sub`), 'rm 目录不带 -r 不得删除'); errs = await errLines(); const rmErr = errs.filter(e => e.includes('是一个目录')); t.eq(rmErr.length, 1, '目录无 -r 报错且只输出一次'); await type('rm -r sub'); // 递归删除 await page.waitForTimeout(120); t.eq(await exists(`${H}/Desktop/Test Dir/sub`), false, 'rm -r 递归删除目录'); await type('rm not-exist.txt'); // 不存在 → 报错一次 await page.waitForTimeout(120); errs = await errLines(); t.eq(errs.filter(e => e.includes('not-exist')).length, 1, '删除不存在文件报错一次'); // ---- 清理 ---- await page.evaluate(h => window.FS.remove(h + '/Desktop/Test Dir'), H); t.eq(env.errors.length, 0, '全程无未捕获异常: ' + env.errors.join(' | ')); }, };