/ clis / zhihu / favorite.test.js
favorite.test.js
  1  import { describe, expect, it, vi } from 'vitest';
  2  import { getRegistry } from '@jackwener/opencli/registry';
  3  import './favorite.js';
  4  describe('zhihu favorite', () => {
  5      it('rejects missing collection selectors before opening the chooser', async () => {
  6          const cmd = getRegistry().get('zhihu/favorite');
  7          expect(cmd?.func).toBeTypeOf('function');
  8          const page = { goto: vi.fn(), evaluate: vi.fn() };
  9          await expect(cmd.func(page, { target: 'article:1', execute: true })).rejects.toMatchObject({
 10              code: 'INVALID_INPUT',
 11          });
 12          expect(page.goto).not.toHaveBeenCalled();
 13          expect(page.evaluate).not.toHaveBeenCalled();
 14      });
 15      it('requires persisted read-back and preserves previously selected collections', async () => {
 16          const cmd = getRegistry().get('zhihu/favorite');
 17          const page = {
 18              goto: vi.fn().mockResolvedValue(undefined),
 19              evaluate: vi.fn()
 20                  .mockResolvedValueOnce({
 21                  chooserRows: [
 22                      { id: 'fav-a', name: '已存在', selected: true },
 23                      { id: 'fav-b', name: '默认收藏夹', selected: false },
 24                  ],
 25                  targetRowId: 'fav-b',
 26                  targetRowName: '默认收藏夹',
 27              })
 28                  .mockResolvedValueOnce({
 29                  persisted: true,
 30                  readbackSource: 'reopened_chooser',
 31                  selectedBefore: ['fav-a'],
 32                  selectedAfter: ['fav-a', 'fav-b'],
 33                  targetSelected: true,
 34              }),
 35          };
 36          await expect(cmd.func(page, { target: 'article:1', collection: '默认收藏夹', execute: true })).resolves.toEqual([
 37              expect.objectContaining({ outcome: 'applied', collection_name: '默认收藏夹', target: 'article:1' }),
 38          ]);
 39          expect(page.evaluate.mock.calls[1][0]).toContain('waitForChooserRows(false)');
 40          expect(page.evaluate.mock.calls[1][0]).toContain("readbackSource");
 41      });
 42      it('requires persisted read-back before returning already_applied', async () => {
 43          const cmd = getRegistry().get('zhihu/favorite');
 44          const page = {
 45              goto: vi.fn().mockResolvedValue(undefined),
 46              evaluate: vi.fn()
 47                  .mockResolvedValueOnce({
 48                  chooserRows: [{ id: 'fav-a', name: '默认收藏夹', selected: true }],
 49                  targetRowId: 'fav-a',
 50                  targetRowName: '默认收藏夹',
 51              })
 52                  .mockResolvedValueOnce({
 53                  persisted: true,
 54                  readbackSource: 'reopened_chooser',
 55                  selectedAfter: ['fav-a'],
 56                  targetSelected: true,
 57              }),
 58          };
 59          await expect(cmd.func(page, { target: 'article:1', collection: '默认收藏夹', execute: true })).resolves.toEqual([
 60              expect.objectContaining({ outcome: 'already_applied', collection_name: '默认收藏夹' }),
 61          ]);
 62      });
 63      it('accepts --collection-id as the stable selector path', async () => {
 64          const cmd = getRegistry().get('zhihu/favorite');
 65          const page = {
 66              goto: vi.fn().mockResolvedValue(undefined),
 67              evaluate: vi.fn()
 68                  .mockResolvedValueOnce({
 69                  chooserRows: [
 70                      { id: 'fav-a', name: '默认收藏夹', selected: false },
 71                      { id: 'fav-b', name: '同名收藏夹', selected: false },
 72                  ],
 73                  targetRowId: 'fav-b',
 74                  targetRowName: null,
 75              })
 76                  .mockResolvedValueOnce({
 77                  persisted: true,
 78                  readbackSource: 'reopened_chooser',
 79                  selectedAfter: ['fav-b'],
 80                  targetSelected: true,
 81              }),
 82          };
 83          await expect(cmd.func(page, { target: 'article:1', 'collection-id': 'fav-b', execute: true })).resolves.toEqual([
 84              expect.objectContaining({ outcome: 'applied', collection_id: 'fav-b' }),
 85          ]);
 86      });
 87      it('rejects duplicate collection names before selecting any row', async () => {
 88          const cmd = getRegistry().get('zhihu/favorite');
 89          const page = {
 90              goto: vi.fn().mockResolvedValue(undefined),
 91              evaluate: vi.fn().mockResolvedValue({
 92                  chooserRows: [
 93                      { id: 'fav-a', name: '默认收藏夹', selected: false },
 94                      { id: 'fav-b', name: '默认收藏夹', selected: false },
 95                  ],
 96              }),
 97          };
 98          await expect(cmd.func(page, { target: 'article:1', collection: '默认收藏夹', execute: true })).rejects.toMatchObject({ code: 'ACTION_NOT_AVAILABLE' });
 99      });
100      it('rejects optimistic chooser state that was not re-read from a reopened chooser', async () => {
101          const cmd = getRegistry().get('zhihu/favorite');
102          const page = {
103              goto: vi.fn().mockResolvedValue(undefined),
104              evaluate: vi.fn()
105                  .mockResolvedValueOnce({
106                  chooserRows: [
107                      { id: 'fav-a', name: '已存在', selected: true },
108                      { id: 'fav-b', name: '默认收藏夹', selected: false },
109                  ],
110                  targetRowId: 'fav-b',
111                  targetRowName: '默认收藏夹',
112              })
113                  .mockResolvedValueOnce({
114                  persisted: true,
115                  readbackSource: 'same_modal',
116                  selectedAfter: ['fav-a', 'fav-b'],
117                  targetSelected: true,
118              }),
119          };
120          await expect(cmd.func(page, { target: 'article:1', collection: '默认收藏夹', execute: true })).rejects.toMatchObject({ code: 'OUTCOME_UNKNOWN' });
121      });
122      it('matches unique collection names even when chooser rows include extra UI text', async () => {
123          const cmd = getRegistry().get('zhihu/favorite');
124          const page = {
125              goto: vi.fn().mockResolvedValue(undefined),
126              evaluate: vi.fn()
127                  .mockResolvedValueOnce({
128                  chooserRows: [
129                      { id: 'fav-b', name: '默认收藏夹 12 条内容', selected: false },
130                  ],
131                  targetRowId: null,
132                  targetRowName: '默认收藏夹',
133              })
134                  .mockResolvedValueOnce({
135                  persisted: true,
136                  readbackSource: 'reopened_chooser',
137                  selectedAfter: ['fav-b'],
138                  targetSelected: true,
139              }),
140          };
141          await expect(cmd.func(page, { target: 'article:1', collection: '默认收藏夹', execute: true })).resolves.toEqual([
142              expect.objectContaining({ outcome: 'applied', collection_name: '默认收藏夹' }),
143          ]);
144          expect(page.evaluate.mock.calls[1][0]).toContain('normalizeCollectionName');
145      });
146      it('normalizes id-less row keys during reopened chooser verification', async () => {
147          const cmd = getRegistry().get('zhihu/favorite');
148          const page = {
149              goto: vi.fn().mockResolvedValue(undefined),
150              evaluate: vi.fn()
151                  .mockResolvedValueOnce({
152                  chooserRows: [
153                      { id: '', name: '默认收藏夹 12 条内容', selected: false },
154                  ],
155                  targetRowId: null,
156                  targetRowName: '默认收藏夹',
157              })
158                  .mockResolvedValueOnce({
159                  persisted: true,
160                  readbackSource: 'reopened_chooser',
161                  selectedAfter: ['name:默认收藏夹'],
162                  targetSelected: true,
163              }),
164          };
165          await expect(cmd.func(page, { target: 'article:1', collection: '默认收藏夹', execute: true })).resolves.toEqual([
166              expect.objectContaining({ outcome: 'applied', collection_name: '默认收藏夹' }),
167          ]);
168          expect(page.evaluate.mock.calls[1][0]).toContain("const rowKey = (row) => row.id || 'name:' + normalizeCollectionName(row.name);");
169          expect(page.evaluate.mock.calls[1][0]).toContain('selectedAfter: chooserRows.filter((row) => row.selected).map(rowKey)');
170      });
171      it('reuses data-attribute answer anchoring during reopened chooser verification', async () => {
172          const cmd = getRegistry().get('zhihu/favorite');
173          const page = {
174              goto: vi.fn().mockResolvedValue(undefined),
175              evaluate: vi.fn()
176                  .mockResolvedValueOnce({
177                  chooserRows: [
178                      { id: 'fav-b', name: '默认收藏夹', selected: false },
179                  ],
180                  targetRowId: 'fav-b',
181                  targetRowName: null,
182              })
183                  .mockResolvedValueOnce({
184                  persisted: true,
185                  readbackSource: 'reopened_chooser',
186                  selectedAfter: ['fav-b'],
187                  targetSelected: true,
188              }),
189          };
190          await expect(cmd.func(page, { target: 'answer:1:2', 'collection-id': 'fav-b', execute: true })).resolves.toEqual([
191              expect.objectContaining({ outcome: 'applied', collection_id: 'fav-b', target: 'answer:1:2' }),
192          ]);
193          expect(page.evaluate.mock.calls[1][0]).toContain("node.getAttribute('data-answerid')");
194          expect(page.evaluate.mock.calls[1][0]).toContain("node.getAttribute('data-zop-question-answer')");
195      });
196  });