Files
jackwener__opencli/clis/zhihu/answer.test.js
jakevin d2974a9ff6 refactor(adapters): convert adapter layer from TypeScript to JavaScript (#928)
* refactor(adapters): convert adapter layer from TypeScript to JavaScript

Core framework stays TypeScript; adapter layer moves to JS-first.
Adapters are essentially "executable config + browser scripts" that
barely use TS features — this simplifies the build/distribution pipeline
by removing the dist/clis/ intermediate compilation step.

Changes:
- Convert all 753 adapter files in clis/ from .ts to .js
- Update tsconfig to exclude clis/ from compilation
- Simplify build-manifest to scan clis/*.js directly (no dist/clis/)
- Update discovery, main, fetch-adapters to load JS adapters from clis/
- Update generate-verified to output .js artifacts
- Update package.json files field: dist/clis/ → clis/
- Fix all test files for the .ts → .js transition

* fix(main): use findPackageRoot for BUILTIN_CLIS path

The previous relative path (../../clis from __dirname) only worked for
dist/src/main.js but broke dev mode (tsx src/main.ts) where __dirname
is <repo>/src — resolving to /clis instead of <repo>/clis.

Use findPackageRoot() which works for both dev and prod paths.
2026-04-10 14:52:18 +08:00

82 lines
4.7 KiB
JavaScript

import { describe, expect, it, vi } from 'vitest';
import { getRegistry } from '@jackwener/opencli/registry';
import './answer.js';
describe('zhihu answer', () => {
it('rejects create mode when the current user already answered the question', async () => {
const cmd = getRegistry().get('zhihu/answer');
expect(cmd?.func).toBeTypeOf('function');
const page = {
goto: vi.fn().mockResolvedValue(undefined),
evaluate: vi.fn()
.mockResolvedValueOnce({ slug: 'alice' })
.mockResolvedValueOnce({ entryPathSafe: false, hasExistingAnswerByCurrentUser: true }),
};
await expect(cmd.func(page, { target: 'question:1', text: 'hello', execute: true })).rejects.toMatchObject({ code: 'ACTION_NOT_AVAILABLE' });
});
it('rejects anonymous mode instead of toggling it', async () => {
const cmd = getRegistry().get('zhihu/answer');
const page = {
goto: vi.fn().mockResolvedValue(undefined),
evaluate: vi.fn()
.mockResolvedValueOnce({ slug: 'alice' })
.mockResolvedValueOnce({ entryPathSafe: true, hasExistingAnswerByCurrentUser: false })
.mockResolvedValueOnce({ editorState: 'fresh_empty', anonymousMode: 'on' }),
};
await expect(cmd.func(page, { target: 'question:1', text: 'hello', execute: true })).rejects.toMatchObject({ code: 'ACTION_NOT_AVAILABLE' });
});
it('rejects when a unique safe answer composer cannot be proven', async () => {
const cmd = getRegistry().get('zhihu/answer');
const page = {
goto: vi.fn().mockResolvedValue(undefined),
evaluate: vi.fn()
.mockResolvedValueOnce({ slug: 'alice' })
.mockResolvedValueOnce({ entryPathSafe: false, hasExistingAnswerByCurrentUser: false }),
};
await expect(cmd.func(page, { target: 'question:1', text: 'hello', execute: true })).rejects.toMatchObject({ code: 'ACTION_NOT_AVAILABLE' });
});
it('rejects when anonymous mode cannot be proven off', async () => {
const cmd = getRegistry().get('zhihu/answer');
const page = {
goto: vi.fn().mockResolvedValue(undefined),
evaluate: vi.fn()
.mockResolvedValueOnce({ slug: 'alice' })
.mockResolvedValueOnce({ entryPathSafe: true, hasExistingAnswerByCurrentUser: false })
.mockResolvedValueOnce({ editorState: 'fresh_empty', anonymousMode: 'unknown' }),
};
await expect(cmd.func(page, { target: 'question:1', text: 'hello', execute: true })).rejects.toMatchObject({ code: 'ACTION_NOT_AVAILABLE' });
});
it('requires a side-effect-free entry path and exact editor content before publish', async () => {
const cmd = getRegistry().get('zhihu/answer');
const page = {
goto: vi.fn().mockResolvedValue(undefined),
evaluate: vi.fn()
.mockResolvedValueOnce({ slug: 'alice' })
.mockResolvedValueOnce({ entryPathSafe: true })
.mockResolvedValueOnce({ editorState: 'fresh_empty', anonymousMode: 'off' })
.mockResolvedValueOnce({ editorContent: 'hello', bodyMatches: true })
.mockResolvedValueOnce({
createdTarget: 'answer:1:2',
createdUrl: 'https://www.zhihu.com/question/1/answer/2',
authorIdentity: 'alice',
bodyMatches: true,
}),
};
await expect(cmd.func(page, { target: 'question:1', text: 'hello', execute: true })).resolves.toEqual([
expect.objectContaining({
outcome: 'created',
created_target: 'answer:1:2',
created_url: 'https://www.zhihu.com/question/1/answer/2',
author_identity: 'alice',
}),
]);
expect(page.evaluate.mock.calls[1][0]).toContain('composerCandidates.length === 1');
expect(page.evaluate.mock.calls[1][0]).not.toContain('writeAnswerButton');
expect(page.evaluate.mock.calls[1][0]).toContain('const readAnswerAuthorSlug = (node) =>');
expect(page.evaluate.mock.calls[1][0]).toContain('const answerAuthorScopeSelector = ".AuthorInfo, .AnswerItem-authorInfo, .ContentItem-meta, [itemprop=\\"author\\"]"');
expect(page.evaluate.mock.calls[1][0]).not.toContain("node.querySelector('a[href^=\"/people/\"]')");
expect(page.evaluate.mock.calls[3][0]).toContain('composerCandidates.length !== 1');
expect(page.evaluate.mock.calls[4][0]).toContain('const readAnswerAuthorSlug = (node) =>');
expect(page.evaluate.mock.calls[4][0]).not.toContain("answerContainer?.querySelector('a[href^=\"/people/\"]')");
});
});