sotashimozono_obsidian-remo.../plugin/tests/WindowSpawner.test.ts
sotashimozono bdd31864cf
test: second coverage pass — Logger/RpcClient/RpcConnection/WindowSpawner (#260)
* test: second coverage pass — Logger/RpcClient/RpcConnection/WindowSpawner

Extended four existing test suites to cover previously-untested branches:

Logger.test.ts:
- setDebug / setMaxLines / clear API methods
- wrapConsole: captures console.warn/error to the file sink as external
  lines, skips [RemoteSSH]-prefixed messages (own plugin traffic),
  idempotency guard
- unwrapConsole: stops capturing after unwrap, is a no-op when unused
- captureExternal: no-op when no file sink is installed
- formatArg: Error → stack/message; non-JSON-serialisable → String()
- installFileSink: mkdirSync failure → fallbackError + early return

RpcClient.test.ts:
- isClosed() false before / true after close
- writeMessage throws → call() rejects with the thrown error (catch block)
- onClose disposer removes the handler before close fires

RpcConnection.test.ts:
- auth RPC result ok:false → AuthInvalid (line 56-57 branch)
- server.info error envelope → cleanupOnFailure (line 65-68 branch)

WindowSpawner.test.ts:
- default opener (no arg constructor) calls window.open with _blank

vitest.config.ts:
- raise thresholds to lines:76 / branches:69 / functions:73

Coverage delta (measured locally):
  Statements: 73.5 % → 74.8 %
  Branches:   68.7 % → 69.7 %
  Functions:  71.1 % → 73.2 %
  Lines:      75.1 % → 76.2 %

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fixup! test(Logger): afterEach closes file sink + strengthen captureExternal assertion

* afterEach now awaits uninstallFileSink() to prevent fd leaks on test
  failure — especially important on Windows where open handles block rmSync
* captureExternal no-op test: add getLines().toHaveLength(0) to verify
  the ring buffer was truly untouched, not just that no throw occurred

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-09 22:09:54 +09:00

50 lines
2 KiB
TypeScript

import { describe, it, expect, vi } from 'vitest';
import { WindowSpawner, type UrlOpener } from '../src/shadow/WindowSpawner';
function recordingOpener() {
const calls: string[] = [];
const opener: UrlOpener = { openUrl(url) { calls.push(url); } };
return { opener, calls };
}
describe('WindowSpawner', () => {
it('builds an obsidian://open?path=… URL with the encoded vault path', () => {
const { opener, calls } = recordingOpener();
const spawner = new WindowSpawner(opener);
const url = spawner.spawn('C:\\Users\\alice\\.obsidian-remote\\vaults\\p1');
expect(calls).toHaveLength(1);
expect(calls[0]).toBe(url);
expect(url.startsWith('obsidian://open?path=')).toBe(true);
expect(url).toContain(encodeURIComponent('C:\\Users\\alice\\.obsidian-remote\\vaults\\p1'));
});
it('encodes characters that would otherwise break the URL', () => {
const { opener, calls } = recordingOpener();
const spawner = new WindowSpawner(opener);
spawner.spawn('/path/with spaces/and?question&amp');
expect(calls[0]).toContain(encodeURIComponent('/path/with spaces/and?question&amp'));
// Spaces / `?` / `&` must NOT appear unencoded after `path=`.
const after = calls[0].slice('obsidian://open?path='.length);
expect(after).not.toMatch(/[ ?&]/);
});
it('returns the URL it fired (so callers can log or surface it)', () => {
const { opener } = recordingOpener();
const url = new WindowSpawner(opener).spawn('/a/b');
expect(url).toBe('obsidian://open?path=' + encodeURIComponent('/a/b'));
});
it('default opener delegates to window.open with _blank target', () => {
const openSpy = vi.spyOn(window, 'open').mockReturnValue(null);
try {
const spawner = new WindowSpawner();
spawner.spawn('/vault/path');
expect(openSpy).toHaveBeenCalledTimes(1);
const [url, target] = openSpy.mock.calls[0];
expect((url as string).startsWith('obsidian://open?path=')).toBe(true);
expect(target).toBe('_blank');
} finally {
openSpy.mockRestore();
}
});
});