1. 信息面板 UI 重做:设计 token 统一、lucide 图标、分区小节、星标/标签/关联 chip 化
2. 关联文档升级:新增 doc:link-batch IPC,A↔B 双向写入 links;面板支持快速双向关联 + 批量勾选关联/解除
3. 代码块内全选复制:Ctrl+A 在代码块内仅全选该代码块(TyporaKeys selectInsideCodeBlock),Ctrl+C 复制纯代码;代码块内 Ctrl+V 按纯文本粘贴
4. 修复 React error #62:btn 改为函数后遗漏 style={btn} 裸用,导致 render 期间更新
94 lines
4.3 KiB
TypeScript
94 lines
4.3 KiB
TypeScript
import { test, expect } from '@playwright/test';
|
|
import { _electron as electron } from 'playwright';
|
|
import { promises as fs } from 'node:fs';
|
|
import nodePath from 'node:path';
|
|
import os from 'node:os';
|
|
|
|
const MAIN_JS = nodePath.join(process.cwd(), 'dist-electron/main/index.js');
|
|
|
|
async function launch(userData: string) {
|
|
const app = await electron.launch({ args: ['--user-data-dir=' + userData, MAIN_JS] });
|
|
const page = await app.firstWindow();
|
|
await page.waitForLoadState('domcontentloaded');
|
|
await page.waitForTimeout(800);
|
|
const invoke = (ch: string, args: any) => page.evaluate(({ ch, args }) => (globalThis as any).dsh.invoke(ch, args), { ch, args });
|
|
return { app, page, invoke };
|
|
}
|
|
|
|
async function setupVault(a: { invoke: any; page: any }, names: string[]) {
|
|
const tmp = await fs.mkdtemp(nodePath.join(os.tmpdir(), 'hn-assoc-'));
|
|
const ud = nodePath.join(tmp, 'ud'); const v = nodePath.join(tmp, 'v');
|
|
await fs.mkdir(ud, { recursive: true }); await fs.mkdir(v, { recursive: true });
|
|
await a.invoke('vault:switch', { root: v });
|
|
for (const n of names) {
|
|
await a.invoke('doc:write', { path: n + '.md', meta: { title: n }, body: '# ' + n });
|
|
}
|
|
await a.page.evaluate(() => (globalThis as any).__STORE__?.getState?.().loadTree());
|
|
await a.page.waitForTimeout(500);
|
|
return { v };
|
|
}
|
|
|
|
async function openInfo(a: { page: any }, docName: string) {
|
|
await a.page.getByTestId('filetree').getByText(docName, { exact: true }).click();
|
|
await a.page.waitForTimeout(500);
|
|
await a.page.getByRole('button', { name: '信息', exact: true }).click();
|
|
await a.page.waitForTimeout(300);
|
|
}
|
|
|
|
// 双向关联:当前文档关联目标后,目标文档的 links 也包含当前文档
|
|
test('关联文档:快速双向关联互相写入 links', async () => {
|
|
const a = await launch(nodePath.join(os.tmpdir(), 'hn-a1-'));
|
|
await setupVault(a, ['DocA', 'DocB', 'DocC']);
|
|
await openInfo(a, 'DocA');
|
|
const select = a.page.locator('select').first();
|
|
await select.selectOption('DocB');
|
|
await a.page.getByRole('button', { name: '关联', exact: true }).click();
|
|
await a.page.waitForTimeout(500);
|
|
await expect(a.page.getByTitle('DocB.md')).toBeVisible({ timeout: 3000 });
|
|
const docb = await a.invoke('doc:read', { path: 'DocB.md' });
|
|
const doca = await a.invoke('doc:read', { path: 'DocA.md' });
|
|
expect(docb.meta.links).toContain('DocA.md');
|
|
expect(doca.meta.links).toContain('DocB.md');
|
|
await a.app.close();
|
|
});
|
|
|
|
// 批量关联:勾选多个文档一次性建立双向关联
|
|
test('关联文档:批量勾选多文档双向关联', async () => {
|
|
const a = await launch(nodePath.join(os.tmpdir(), 'hn-a2-'));
|
|
await setupVault(a, ['DocA', 'DocB', 'DocC']);
|
|
await openInfo(a, 'DocA');
|
|
await a.page.getByRole('button', { name: /批量关联/ }).click();
|
|
await a.page.waitForTimeout(200);
|
|
const boxes = a.page.locator('input[type=checkbox]');
|
|
const count = await boxes.count();
|
|
expect(count).toBeGreaterThanOrEqual(2);
|
|
await boxes.nth(0).check();
|
|
await boxes.nth(1).check();
|
|
await a.page.getByRole('button', { name: /关联选中/ }).click();
|
|
await a.page.waitForTimeout(600);
|
|
const doca = await a.invoke('doc:read', { path: 'DocA.md' });
|
|
const docb = await a.invoke('doc:read', { path: 'DocB.md' });
|
|
const docc = await a.invoke('doc:read', { path: 'DocC.md' });
|
|
expect(doca.meta.links).toContain('DocB.md');
|
|
expect(doca.meta.links).toContain('DocC.md');
|
|
expect(docb.meta.links).toContain('DocA.md');
|
|
expect(docc.meta.links).toContain('DocA.md');
|
|
await a.app.close();
|
|
});
|
|
|
|
// 解除关联为双向:移除后两侧 links 均清除
|
|
test('关联文档:解除关联双向清除', async () => {
|
|
const a = await launch(nodePath.join(os.tmpdir(), 'hn-a3-'));
|
|
await setupVault(a, ['DocA', 'DocB']);
|
|
await a.invoke('doc:link-batch', { path: 'DocA.md', targets: ['DocB.md'], remove: true });
|
|
await a.invoke('doc:link-batch', { path: 'DocA.md', targets: ['DocB.md'], remove: false });
|
|
await openInfo(a, 'DocA');
|
|
await expect(a.page.getByTitle('DocB.md')).toBeVisible({ timeout: 3000 });
|
|
await a.page.getByTitle('解除关联(双向)').click();
|
|
await a.page.waitForTimeout(500);
|
|
const doca = await a.invoke('doc:read', { path: 'DocA.md' });
|
|
const docb = await a.invoke('doc:read', { path: 'DocB.md' });
|
|
expect(doca.meta.links ?? []).not.toContain('DocB.md');
|
|
expect(docb.meta.links ?? []).not.toContain('DocA.md');
|
|
await a.app.close();
|
|
}); |