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(); });