utils.test.js
1 import { describe, expect, it } from 'vitest'; 2 import { buildNotebooklmRpcBody, classifyNotebooklmPage, extractNotebooklmHistoryPreview, extractNotebooklmRpcResult, getNotebooklmPageState, normalizeNotebooklmTitle, parseNotebooklmHistoryThreadIdsResult, parseNotebooklmIdFromUrl, parseNotebooklmListResult, parseNotebooklmNoteListRawRows, parseNotebooklmNotebookDetailResult, parseNotebooklmSourceFulltextResult, parseNotebooklmSourceGuideResult, parseNotebooklmSourceListResult, } from './utils.js'; 3 describe('notebooklm utils', () => { 4 it('parses notebook id from a notebook url', () => { 5 expect(parseNotebooklmIdFromUrl('https://notebooklm.google.com/notebook/abc-123')).toBe('abc-123'); 6 }); 7 it('returns empty string when notebook id is absent', () => { 8 expect(parseNotebooklmIdFromUrl('https://notebooklm.google.com/')).toBe(''); 9 }); 10 it('classifies notebook pages correctly', () => { 11 expect(classifyNotebooklmPage('https://notebooklm.google.com/notebook/demo-id')).toBe('notebook'); 12 expect(classifyNotebooklmPage('https://notebooklm.google.com/')).toBe('home'); 13 expect(classifyNotebooklmPage('https://example.com/notebook/demo-id')).toBe('unknown'); 14 }); 15 it('normalizes notebook titles', () => { 16 expect(normalizeNotebooklmTitle(' Demo Notebook ')).toBe('Demo Notebook'); 17 expect(normalizeNotebooklmTitle('', 'Untitled')).toBe('Untitled'); 18 }); 19 it('builds the notebooklm rpc request body with csrf token', () => { 20 const body = buildNotebooklmRpcBody('wXbhsf', [null, 1, null, [2]], 'csrf123'); 21 expect(body).toContain('f.req='); 22 expect(body).toContain('at=csrf123'); 23 expect(body.endsWith('&')).toBe(true); 24 expect(decodeURIComponent(body)).toContain('"[null,1,null,[2]]"'); 25 }); 26 it('extracts notebooklm rpc payload from chunked batchexecute response', () => { 27 const raw = ')]}\'\n107\n[["wrb.fr","wXbhsf","[[[\\"Notebook One\\",null,\\"nb1\\",null,null,[null,false,null,null,null,[1704067200]]]]]"]]'; 28 const result = extractNotebooklmRpcResult(raw, 'wXbhsf'); 29 expect(Array.isArray(result)).toBe(true); 30 expect(result[0]).toBeDefined(); 31 }); 32 it('parses notebook rows from notebooklm rpc payload', () => { 33 const rows = parseNotebooklmListResult([ 34 [ 35 ['Notebook One', null, 'nb1', null, null, [null, false, null, null, null, [1704067200]]], 36 ], 37 ]); 38 expect(rows).toEqual([ 39 { 40 id: 'nb1', 41 title: 'Notebook One', 42 url: 'https://notebooklm.google.com/notebook/nb1', 43 source: 'rpc', 44 is_owner: true, 45 created_at: '2024-01-01T00:00:00.000Z', 46 }, 47 ]); 48 }); 49 it('parses notebook metadata from notebook detail rpc payload', () => { 50 const notebook = parseNotebooklmNotebookDetailResult([ 51 'Browser Automation', 52 [ 53 [ 54 [['src1']], 55 'Pasted text', 56 [null, 359, [1774872183, 855096000], ['doc1', [1774872183, 356519000]], 8, null, 1, null, null, null, null, null, null, null, [1774872185, 395271000]], 57 [null, 2], 58 ], 59 ], 60 'nb-demo', 61 '🕸️', 62 null, 63 [1, false, true, null, null, [1774889558, 348721000], 1, false, [1774872161, 361922000], null, null, null, false, true, 1, false, null, true, 1], 64 ]); 65 expect(notebook).toEqual({ 66 id: 'nb-demo', 67 title: 'Browser Automation', 68 url: 'https://notebooklm.google.com/notebook/nb-demo', 69 source: 'rpc', 70 emoji: '🕸️', 71 source_count: 1, 72 is_owner: true, 73 created_at: '2026-03-30T12:02:41.361Z', 74 updated_at: '2026-03-30T16:52:38.348Z', 75 }); 76 }); 77 it('parses notebook metadata when detail rpc wraps the payload in a singleton envelope', () => { 78 const notebook = parseNotebooklmNotebookDetailResult([ 79 [ 80 'Browser Automation', 81 [ 82 [ 83 [['src1']], 84 'Pasted text', 85 [null, 359, [1774872183, 855096000], ['doc1', [1774872183, 356519000]], 8, null, 1, null, null, null, null, null, null, null, [1774872185, 395271000]], 86 [null, 2], 87 ], 88 ], 89 'nb-demo', 90 '🕸️', 91 null, 92 [1, false, true, null, null, [1774889558, 348721000], 1, false, [1774872161, 361922000], null, null, null, false, true, 1, false, null, true, 1], 93 ], 94 ]); 95 expect(notebook).toEqual({ 96 id: 'nb-demo', 97 title: 'Browser Automation', 98 url: 'https://notebooklm.google.com/notebook/nb-demo', 99 source: 'rpc', 100 emoji: '🕸️', 101 source_count: 1, 102 is_owner: true, 103 created_at: '2026-03-30T12:02:41.361Z', 104 updated_at: '2026-03-30T16:52:38.348Z', 105 }); 106 }); 107 it('parses sources from notebook detail rpc payload', () => { 108 const rows = parseNotebooklmSourceListResult([ 109 'Browser Automation', 110 [ 111 [ 112 [['src1']], 113 'Pasted text', 114 [null, 359, [1774872183, 855096000], ['doc1', [1774872183, 356519000]], 8, null, 1, null, null, null, null, null, null, null, [1774872185, 395271000]], 115 [null, 2], 116 ], 117 ], 118 'nb-demo', 119 '🕸️', 120 null, 121 [1, false, true, null, null, [1774889558, 348721000], 1, false, [1774872161, 361922000], null, null, null, false, true, 1, false, null, true, 1], 122 ]); 123 expect(rows).toEqual([ 124 { 125 id: 'src1', 126 notebook_id: 'nb-demo', 127 title: 'Pasted text', 128 type: 'pasted-text', 129 type_code: 8, 130 size: 359, 131 created_at: '2026-03-30T12:03:03.855Z', 132 updated_at: '2026-03-30T12:03:05.395Z', 133 url: 'https://notebooklm.google.com/notebook/nb-demo', 134 source: 'rpc', 135 }, 136 ]); 137 }); 138 it('parses sources when detail rpc wraps the payload in a singleton envelope', () => { 139 const rows = parseNotebooklmSourceListResult([ 140 [ 141 'Browser Automation', 142 [ 143 [ 144 [['src1']], 145 'Pasted text', 146 [null, 359, [1774872183, 855096000], ['doc1', [1774872183, 356519000]], 8, null, 1, null, null, null, null, null, null, null, [1774872185, 395271000]], 147 [null, 2], 148 ], 149 ], 150 'nb-demo', 151 '🕸️', 152 null, 153 [1, false, true, null, null, [1774889558, 348721000], 1, false, [1774872161, 361922000], null, null, null, false, true, 1, false, null, true, 1], 154 ], 155 ]); 156 expect(rows).toEqual([ 157 { 158 id: 'src1', 159 notebook_id: 'nb-demo', 160 title: 'Pasted text', 161 type: 'pasted-text', 162 type_code: 8, 163 size: 359, 164 created_at: '2026-03-30T12:03:03.855Z', 165 updated_at: '2026-03-30T12:03:05.395Z', 166 url: 'https://notebooklm.google.com/notebook/nb-demo', 167 source: 'rpc', 168 }, 169 ]); 170 }); 171 it('parses sources when the source id container is only wrapped once', () => { 172 const rows = parseNotebooklmSourceListResult([ 173 [ 174 'Browser Automation', 175 [ 176 [ 177 ['src-live'], 178 'Pasted text', 179 [null, 359, [1774872183, 855096000], ['doc1', [1774872183, 356519000]], 8, null, 1, null, null, null, null, null, null, null, [1774872185, 395271000]], 180 [null, 2], 181 ], 182 ], 183 'nb-demo', 184 '🕸️', 185 null, 186 [1, false, true, null, null, [1774889558, 348721000], 1, false, [1774872161, 361922000], null, null, null, false, true, 1, false, null, true, 1], 187 ], 188 ]); 189 expect(rows).toEqual([ 190 { 191 id: 'src-live', 192 notebook_id: 'nb-demo', 193 title: 'Pasted text', 194 type: 'pasted-text', 195 type_code: 8, 196 size: 359, 197 created_at: '2026-03-30T12:03:03.855Z', 198 updated_at: '2026-03-30T12:03:05.395Z', 199 url: 'https://notebooklm.google.com/notebook/nb-demo', 200 source: 'rpc', 201 }, 202 ]); 203 }); 204 it('parses source type from metadata slot instead of the stale entry[3] envelope', () => { 205 const rows = parseNotebooklmSourceListResult([ 206 [ 207 'Browser Automation', 208 [ 209 [ 210 ['src-pdf'], 211 'Manual.pdf', 212 [null, 18940, [1774872183, 855096000], ['doc1', [1774872183, 356519000]], 3, null, 1, null, null, null, null, null, null, null, [1774872185, 395271000]], 213 [null, 2], 214 ], 215 [ 216 ['src-web'], 217 'Example Site', 218 [null, 131, [1774872183, 855096000], ['doc2', [1774872183, 356519000]], 5, ['https://example.com'], 1, null, null, null, null, null, null, null, [1774872185, 395271000]], 219 [null, 2], 220 ], 221 [ 222 ['src-yt'], 223 'Video Source', 224 [null, 11958, [1774872183, 855096000], ['doc3', [1774872183, 356519000]], 9, ['https://youtu.be/demo', 'demo', 'Uploader'], 1, null, null, null, null, null, null, null, [1774872185, 395271000]], 225 [null, 2], 226 ], 227 ], 228 'nb-demo', 229 '🕸️', 230 null, 231 [1, false, true, null, null, [1774889558, 348721000], 1, false, [1774872161, 361922000], null, null, null, false, true, 1, false, null, true, 1], 232 ], 233 ]); 234 expect(rows).toEqual([ 235 expect.objectContaining({ 236 id: 'src-pdf', 237 type: 'pdf', 238 type_code: 3, 239 }), 240 expect.objectContaining({ 241 id: 'src-web', 242 type: 'web', 243 type_code: 5, 244 }), 245 expect.objectContaining({ 246 id: 'src-yt', 247 type: 'youtube', 248 type_code: 9, 249 }), 250 ]); 251 }); 252 it('parses notebook history thread ids from hPTbtc payload', () => { 253 const threadIds = parseNotebooklmHistoryThreadIdsResult([ 254 [[['28e0f2cb-4591-45a3-a661-7653666f7c78']]], 255 ]); 256 expect(threadIds).toEqual(['28e0f2cb-4591-45a3-a661-7653666f7c78']); 257 }); 258 it('extracts a notebook history preview from khqZz payload', () => { 259 const preview = extractNotebooklmHistoryPreview([ 260 [ 261 ['28e0f2cb-4591-45a3-a661-7653666f7c78'], 262 [null, 'Summarize this notebook'], 263 ], 264 ]); 265 expect(preview).toBe('Summarize this notebook'); 266 }); 267 it('parses notebook notes from studio note rows', () => { 268 const rows = parseNotebooklmNoteListRawRows([ 269 { 270 title: '新建笔记', 271 text: 'sticky_note_2 新建笔记 6 分钟前 more_vert', 272 }, 273 ], 'nb-demo', 'https://notebooklm.google.com/notebook/nb-demo'); 274 expect(rows).toEqual([ 275 { 276 notebook_id: 'nb-demo', 277 title: '新建笔记', 278 created_at: '6 分钟前', 279 url: 'https://notebooklm.google.com/notebook/nb-demo', 280 source: 'studio-list', 281 }, 282 ]); 283 }); 284 it('parses source fulltext from hizoJc payload', () => { 285 const row = parseNotebooklmSourceFulltextResult([ 286 [ 287 [['src-1']], 288 '粘贴的文字', 289 [null, 359, [1774872183, 855096000], null, 8, null, 1, ['https://example.com/source']], 290 [null, 2], 291 ], 292 null, 293 null, 294 [ 295 [ 296 [ 297 [0, 5, [[[0, 5, ['第一段']]]]], 298 [5, 10, [[[5, 10, ['第二段']]]]], 299 ], 300 ], 301 ], 302 ], 'nb-demo', 'https://notebooklm.google.com/notebook/nb-demo'); 303 expect(row).toEqual({ 304 source_id: 'src-1', 305 notebook_id: 'nb-demo', 306 title: '粘贴的文字', 307 kind: 'pasted-text', 308 content: '第一段\n第二段', 309 char_count: 7, 310 url: 'https://example.com/source', 311 source: 'rpc', 312 }); 313 }); 314 it('parses source guide from tr032e payloads with either null or source-id envelope in slot 0', () => { 315 const source = { 316 id: 'src-yt', 317 notebook_id: 'nb-demo', 318 title: 'Video Source', 319 type: 'youtube', 320 }; 321 expect(parseNotebooklmSourceGuideResult([ 322 [ 323 [ 324 null, 325 ['Guide summary'], 326 [['AI', 'agents']], 327 [], 328 ], 329 ], 330 ], source)).toEqual({ 331 source_id: 'src-yt', 332 notebook_id: 'nb-demo', 333 title: 'Video Source', 334 type: 'youtube', 335 summary: 'Guide summary', 336 keywords: ['AI', 'agents'], 337 source: 'rpc', 338 }); 339 expect(parseNotebooklmSourceGuideResult([ 340 [ 341 [ 342 [['src-yt']], 343 ['Guide summary'], 344 [['AI', 'agents']], 345 [], 346 ], 347 ], 348 ], source)).toEqual({ 349 source_id: 'src-yt', 350 notebook_id: 'nb-demo', 351 title: 'Video Source', 352 type: 'youtube', 353 summary: 'Guide summary', 354 keywords: ['AI', 'agents'], 355 source: 'rpc', 356 }); 357 }); 358 it('prefers real NotebookLM page tokens over login text heuristics', async () => { 359 let call = 0; 360 const page = { 361 evaluate: async () => { 362 call += 1; 363 if (call === 1) { 364 return { 365 url: 'https://notebooklm.google.com/notebook/nb-demo', 366 title: 'Demo Notebook - NotebookLM', 367 hostname: 'notebooklm.google.com', 368 kind: 'notebook', 369 notebookId: 'nb-demo', 370 loginRequired: true, 371 notebookCount: 0, 372 }; 373 } 374 return { 375 html: '<html>"SNlM0e":"csrf-123","FdrFJe":"sess-456"</html>', 376 sourcePath: '/notebook/nb-demo', 377 }; 378 }, 379 }; 380 await expect(getNotebooklmPageState(page)).resolves.toEqual({ 381 url: 'https://notebooklm.google.com/notebook/nb-demo', 382 title: 'Demo Notebook - NotebookLM', 383 hostname: 'notebooklm.google.com', 384 kind: 'notebook', 385 notebookId: 'nb-demo', 386 loginRequired: false, 387 notebookCount: 0, 388 }); 389 }); 390 });