pal-crawl 1.8.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/pal.test.js CHANGED
@@ -148,6 +148,107 @@ const SEARCH_RESULT_HTML = `
148
148
  </tbody>
149
149
  </table>
150
150
  `;
151
+ // ─── NSM HTML Fixtures ────────────────────────────────────────────────────────
152
+ /** 위원회에 배속된 법안 한 건 + 페이지네이션 */
153
+ const NSM_LIST_HTML = `
154
+ <p class="numBuild">총 <span>3,142</span>건</p>
155
+ <p class="numBuild">현재 <em>2</em>/<span>315</span>쪽</p>
156
+ <table>
157
+ <tbody>
158
+ <tr>
159
+ <td data-th="의안명">
160
+ <a href="/gcom/nsmLmSts/out/2219088/detailRP">인공지능 기본법 일부개정법률안</a>
161
+ </td>
162
+ <td data-th="제안자(제안일자)">
163
+ <p>홍길동의원 등 10인</p>
164
+ <p>(2026.05.01.)</p>
165
+ </td>
166
+ <td data-th="상임위원회(소관부처)">
167
+ <p>과학기술정보방송통신위원회</p>
168
+ <p>(과학기술정보통신부)</p>
169
+ </td>
170
+ <td data-th="국회현황(추진일자)">
171
+ <p>위원회 회부</p>
172
+ <p>(2026.05.03.)</p>
173
+ </td>
174
+ <td data-th="의결현황(의결일자)">
175
+ <p>원안가결</p>
176
+ <p>(2026.05.20.)</p>
177
+ </td>
178
+ </tr>
179
+ </tbody>
180
+ </table>
181
+ `;
182
+ /** 위원회 회부 이전 발의 상태 법안 (committee 빈 문자열) */
183
+ const NSM_LIST_HTML_PENDING = `
184
+ <p class="numBuild">총 <span>87</span>건</p>
185
+ <p class="numBuild">현재 <em>1</em>/<span>9</span>쪽</p>
186
+ <table>
187
+ <tbody>
188
+ <tr>
189
+ <td data-th="의안명">
190
+ <a href="/gcom/nsmLmSts/out/2219200/detailRP">개인정보 보호법 일부개정법률안</a>
191
+ </td>
192
+ <td data-th="제안자(제안일자)">
193
+ <p>김철수의원 등 5인</p>
194
+ <p>(2026.05.28.)</p>
195
+ </td>
196
+ <td data-th="상임위원회(소관부처)">
197
+ <p>(개인정보보호위원회)</p>
198
+ </td>
199
+ <td data-th="국회현황(추진일자)">
200
+ <p>발의</p>
201
+ <p>(2026.05.28.)</p>
202
+ </td>
203
+ <td data-th="의결현황(의결일자)"></td>
204
+ </tr>
205
+ </tbody>
206
+ </table>
207
+ `;
208
+ /** 상세 페이지 */
209
+ const NSM_DETAIL_HTML = `
210
+ <h2></h2>
211
+ <h2>인공지능 기본법 일부개정법률안</h2>
212
+ <h3>기본정보</h3>
213
+ <h3>국회입법현황</h3>
214
+ <table>
215
+ <tbody>
216
+ <tr>
217
+ <th>발의정보</th>
218
+ <td>홍길동의원 등 10인, 제2219088호(2026. 5. 1.). 제435회 국회(임시회)</td>
219
+ </tr>
220
+ <tr>
221
+ <th>의안원문</th>
222
+ <td>
223
+ <button onclick="fnDownload(99001)">인공지능기본법안.hwp<span class="a11y_hidden">다운로드</span></button>
224
+ <button onclick="fnDownload(99002)">인공지능기본법안검토보고서.pdf<span class="a11y_hidden">다운로드</span></button>
225
+ </td>
226
+ </tr>
227
+ <tr>
228
+ <th>제안이유 및 주요내용</th>
229
+ <td>
230
+ <pre>인공지능 기술의 발전에 따라<br/>규제 체계를 정비하고자 함</pre>
231
+ </td>
232
+ </tr>
233
+ </tbody>
234
+ </table>
235
+ `;
236
+ /** 상세 페이지 - 제안이유 없음 (pre 태그 없는 경우) */
237
+ const NSM_DETAIL_HTML_NO_REASON = `
238
+ <h2>최소 법률안</h2>
239
+ <table>
240
+ <tbody>
241
+ <tr>
242
+ <th>발의정보</th>
243
+ <td>이영희의원, 제2200001호(2026. 1. 5.). 제433회 국회(임시회)</td>
244
+ </tr>
245
+ <tr>
246
+ <th>의안원문</th>
247
+ <td></td>
248
+ </tr>
249
+ </tbody>
250
+ </table>
251
+ `;
151
252
  // ─── PalParser ──────────────────────────────────────────────────────────────────────────────
152
253
  describe('PalParser', () => {
153
254
  const parser = new parser_1.PalParser();
@@ -198,7 +299,7 @@ describe('PalParser', () => {
198
299
  });
199
300
  test('strips the repeated heading line from proposal reason', () => {
200
301
  const content = parser.parseContent(CONTENT_HTML);
201
- // The desc starts with "제안이유 및 주요내용" which matches the heading — it must be stripped.
302
+ // The desc starts with "제안이유 및 주요내용" which matches the heading - it must be stripped.
202
303
  expect(content.proposalReason).not.toMatch(/제안이유/);
203
304
  });
204
305
  test('returns empty title and all nulls for empty html', () => {
@@ -361,9 +462,10 @@ describe('PalCrawl', () => {
361
462
  expect(listHtml.length).toBeGreaterThan(0);
362
463
  expect(listHtml).not.toContain('�');
363
464
  });
364
- test('get: returns 10 items per page', async () => {
465
+ test('get: returns items (up to 10 per page)', async () => {
365
466
  const result = await palCrawl.get();
366
- expect(result).toHaveLength(10);
467
+ expect(result.length).toBeGreaterThan(0);
468
+ expect(result.length).toBeLessThanOrEqual(10);
367
469
  });
368
470
  test('get: each item has valid structure and no garbled text', () => {
369
471
  ongoingTable.forEach((e) => {
@@ -466,10 +568,10 @@ describe('PalCrawl', () => {
466
568
  expect(result.items.length).toBeGreaterThan(0);
467
569
  });
468
570
  test('search: billName filter returns matching items', async () => {
469
- const result = await palCrawl.search({ billName: '도로교통' });
571
+ const result = await palCrawl.search({ billName: '개정법률' });
470
572
  expect(result.items.length).toBeGreaterThan(0);
471
573
  result.items.forEach((item) => {
472
- expect(item.subject).toMatch(/도로교통/);
574
+ expect(item.subject).toMatch(/개정법률/);
473
575
  });
474
576
  });
475
577
  test('search: sortCol=BILL_NAME sortGbn=ASC returns alphabetically sorted items', async () => {
@@ -510,17 +612,17 @@ describe('PalCrawl', () => {
510
612
  expect(page1).toHaveLength(legacy.length);
511
613
  expect(page1[0].subject).toBe(legacy[0].subject);
512
614
  });
513
- test('getPage(2): returns different items from page 1', async () => {
615
+ test('getDonePage(2): returns different items from page 1', async () => {
514
616
  const [page1, page2] = await Promise.all([
515
- palCrawl.getPage(1),
516
- palCrawl.getPage(2),
617
+ palCrawl.getDonePage(1),
618
+ palCrawl.getDonePage(2),
517
619
  ]);
518
620
  expect(page1.length).toBeGreaterThan(0);
519
621
  expect(page2.length).toBeGreaterThan(0);
520
622
  expect(page1[0].subject).not.toBe(page2[0].subject);
521
623
  });
522
- test('getPage: pageUnit=20 returns up to 20 items', async () => {
523
- const items = await palCrawl.getPage(1, 20);
624
+ test('getDonePage: pageUnit=20 returns up to 20 items', async () => {
625
+ const items = await palCrawl.getDonePage(1, 20);
524
626
  expect(items.length).toBeLessThanOrEqual(20);
525
627
  expect(items.length).toBeGreaterThan(10);
526
628
  });
@@ -543,18 +645,18 @@ describe('PalCrawl', () => {
543
645
  expect(pages[0].items.length).toBeGreaterThan(0);
544
646
  expect(pages[1].items.length).toBeGreaterThan(0);
545
647
  });
546
- test('getAllPages: currentPage increments correctly', async () => {
648
+ test('getAllDonePages: currentPage increments correctly', async () => {
547
649
  const pages = [];
548
- for await (const page of palCrawl.getAllPages({ pageUnit: 5 }, { maxPages: 3, delayMs: 0 })) {
650
+ for await (const page of palCrawl.getAllDonePages({ pageUnit: 5 }, { maxPages: 3, delayMs: 0 })) {
549
651
  pages.push(page);
550
652
  }
551
653
  expect(pages[0].currentPage).toBe(1);
552
654
  expect(pages[1].currentPage).toBe(2);
553
655
  expect(pages[2].currentPage).toBe(3);
554
656
  });
555
- test('getAllPages: total and totalPages are consistent across pages', async () => {
657
+ test('getAllDonePages: total and totalPages are consistent across pages', async () => {
556
658
  const pages = [];
557
- for await (const page of palCrawl.getAllPages({}, { maxPages: 2, delayMs: 0 })) {
659
+ for await (const page of palCrawl.getAllDonePages({}, { maxPages: 2, delayMs: 0 })) {
558
660
  pages.push(page);
559
661
  }
560
662
  expect(pages[0].total).toBe(pages[1].total);
@@ -567,9 +669,9 @@ describe('PalCrawl', () => {
567
669
  }
568
670
  expect(pages[0].items[0].subject).not.toBe(pages[1].items[0].subject);
569
671
  });
570
- test('getAllPages: concurrency > 1 yields pages in order', async () => {
672
+ test('getAllDonePages: concurrency > 1 yields pages in order', async () => {
571
673
  const pages = [];
572
- for await (const page of palCrawl.getAllPages({ pageUnit: 5 }, { maxPages: 4, delayMs: 0, concurrency: 2 })) {
674
+ for await (const page of palCrawl.getAllDonePages({ pageUnit: 5 }, { maxPages: 4, delayMs: 0, concurrency: 2 })) {
573
675
  pages.push(page);
574
676
  }
575
677
  expect(pages).toHaveLength(4);
@@ -587,7 +689,7 @@ describe('PalCrawl', () => {
587
689
  expect(pages[0].items[0].subject).not.toBe(pages[1].items[0].subject);
588
690
  });
589
691
  });
590
- // ── Screenshots ──────────────────────────────────────────────────────────────
692
+ // ── Screenshot functionality ─────────────────────────────────────────────────
591
693
  describe('Screenshot functionality', () => {
592
694
  test('creates instance with screenshot config', () => {
593
695
  const config = {
@@ -632,4 +734,421 @@ describe('PalCrawl', () => {
632
734
  });
633
735
  });
634
736
  });
737
+ // ─── NsmLmStsParser ──────────────────────────────────────────────────────────
738
+ describe('NsmLmStsParser', () => {
739
+ const parser = new parser_1.NsmLmStsParser();
740
+ // ── parseList ──────────────────────────────────────────────────────────────
741
+ describe('parseList', () => {
742
+ test('returns empty result for empty string', () => {
743
+ const result = parser.parseList('');
744
+ expect(result.total).toBe(0);
745
+ expect(result.totalPages).toBe(0);
746
+ expect(result.currentPage).toBe(1);
747
+ expect(result.items).toHaveLength(0);
748
+ });
749
+ test('returns empty result for invalid html', () => {
750
+ const result = parser.parseList('not html at all');
751
+ expect(result.items).toHaveLength(0);
752
+ });
753
+ test('parses pagination: total, totalPages, currentPage', () => {
754
+ const result = parser.parseList(NSM_LIST_HTML);
755
+ expect(result.total).toBe(3142);
756
+ expect(result.totalPages).toBe(315);
757
+ expect(result.currentPage).toBe(2);
758
+ });
759
+ test('parses bill name and link', () => {
760
+ const result = parser.parseList(NSM_LIST_HTML);
761
+ expect(result.items).toHaveLength(1);
762
+ const item = result.items[0];
763
+ expect(item.billName).toBe('인공지능 기본법 일부개정법률안');
764
+ expect(item.billNo).toBe('2219088');
765
+ expect(item.link).toContain('2219088/detailRP');
766
+ });
767
+ test('parses proposer and proposalDate (strips parentheses)', () => {
768
+ const item = parser.parseList(NSM_LIST_HTML).items[0];
769
+ expect(item.proposer).toBe('홍길동의원 등 10인');
770
+ expect(item.proposalDate).toBe('2026.05.01.');
771
+ });
772
+ test('parses committee and ministry', () => {
773
+ const item = parser.parseList(NSM_LIST_HTML).items[0];
774
+ expect(item.committee).toBe('과학기술정보방송통신위원회');
775
+ expect(item.ministry).toBe('과학기술정보통신부');
776
+ });
777
+ test('parses progressStatus and progressDate', () => {
778
+ const item = parser.parseList(NSM_LIST_HTML).items[0];
779
+ expect(item.progressStatus).toBe('위원회 회부');
780
+ expect(item.progressDate).toBe('2026.05.03.');
781
+ });
782
+ test('parses resolutionStatus and resolutionDate', () => {
783
+ const item = parser.parseList(NSM_LIST_HTML).items[0];
784
+ expect(item.resolutionStatus).toBe('원안가결');
785
+ expect(item.resolutionDate).toBe('2026.05.20.');
786
+ });
787
+ test('pending bill: committee is empty string, ministry is set', () => {
788
+ const item = parser.parseList(NSM_LIST_HTML_PENDING).items[0];
789
+ expect(item.committee).toBe('');
790
+ expect(item.ministry).toBe('개인정보보호위원회');
791
+ });
792
+ test('pending bill: resolutionStatus and resolutionDate are empty strings', () => {
793
+ const item = parser.parseList(NSM_LIST_HTML_PENDING).items[0];
794
+ expect(item.resolutionStatus).toBe('');
795
+ expect(item.resolutionDate).toBe('');
796
+ });
797
+ test('pending bill: progressStatus is "발의"', () => {
798
+ const item = parser.parseList(NSM_LIST_HTML_PENDING).items[0];
799
+ expect(item.progressStatus).toBe('발의');
800
+ });
801
+ test('falls back to items.length when pagination markup is absent', () => {
802
+ const minimalHtml = `
803
+ <table><tbody>
804
+ <tr>
805
+ <td data-th="의안명"><a href="/gcom/nsmLmSts/out/1/detailRP">A</a></td>
806
+ <td data-th="제안자(제안일자)"><p>홍길동</p><p>(2026.01.01.)</p></td>
807
+ <td data-th="상임위원회(소관부처)"></td>
808
+ <td data-th="국회현황(추진일자)"><p>발의</p></td>
809
+ <td data-th="의결현황(의결일자)"></td>
810
+ </tr>
811
+ </tbody></table>`;
812
+ const result = parser.parseList(minimalHtml);
813
+ expect(result.totalPages).toBe(1);
814
+ expect(result.total).toBe(1);
815
+ expect(result.currentPage).toBe(1);
816
+ });
817
+ test('skips rows without a bill name', () => {
818
+ const htmlNoName = `
819
+ <table><tbody>
820
+ <tr>
821
+ <td data-th="의안명"><a href="/gcom/nsmLmSts/out/1/detailRP"></a></td>
822
+ <td data-th="제안자(제안일자)"></td>
823
+ <td data-th="상임위원회(소관부처)"></td>
824
+ <td data-th="국회현황(추진일자)"></td>
825
+ <td data-th="의결현황(의결일자)"></td>
826
+ </tr>
827
+ </tbody></table>`;
828
+ expect(parser.parseList(htmlNoName).items).toHaveLength(0);
829
+ });
830
+ });
831
+ // ── parseDetail ────────────────────────────────────────────────────────────
832
+ describe('parseDetail', () => {
833
+ test('returns default empty object for empty string', () => {
834
+ const detail = parser.parseDetail('');
835
+ expect(detail.title).toBe('');
836
+ expect(detail.billNo).toBe('');
837
+ expect(detail.proposer).toBe('');
838
+ expect(detail.proposalDate).toBe('');
839
+ expect(detail.session).toBe('');
840
+ expect(detail.proposalInfo).toBe('');
841
+ expect(detail.proposalReason).toBeNull();
842
+ expect(detail.attachments).toHaveLength(0);
843
+ });
844
+ test('picks title h2 (skips empty h2; h3 elements are section headers)', () => {
845
+ const detail = parser.parseDetail(NSM_DETAIL_HTML);
846
+ expect(detail.title).toBe('인공지능 기본법 일부개정법률안');
847
+ });
848
+ test('parses proposalInfo raw text', () => {
849
+ const detail = parser.parseDetail(NSM_DETAIL_HTML);
850
+ expect(detail.proposalInfo).toContain('제2219088호');
851
+ expect(detail.proposalInfo).toContain('제435회 국회(임시회)');
852
+ });
853
+ test('extracts billNo from proposalInfo', () => {
854
+ const detail = parser.parseDetail(NSM_DETAIL_HTML);
855
+ expect(detail.billNo).toBe('2219088');
856
+ });
857
+ test('extracts proposer from proposalInfo', () => {
858
+ const detail = parser.parseDetail(NSM_DETAIL_HTML);
859
+ expect(detail.proposer).toBe('홍길동의원 등 10인');
860
+ });
861
+ test('extracts proposalDate from proposalInfo', () => {
862
+ const detail = parser.parseDetail(NSM_DETAIL_HTML);
863
+ expect(detail.proposalDate).toBe('2026. 5. 1.');
864
+ });
865
+ test('extracts session from proposalInfo', () => {
866
+ const detail = parser.parseDetail(NSM_DETAIL_HTML);
867
+ expect(detail.session).toBe('제435회 국회(임시회)');
868
+ });
869
+ test('parses attachments: fileId and filename (strips a11y_hidden span)', () => {
870
+ const detail = parser.parseDetail(NSM_DETAIL_HTML);
871
+ expect(detail.attachments).toHaveLength(2);
872
+ expect(detail.attachments[0].fileId).toBe('99001');
873
+ expect(detail.attachments[0].filename).toBe('인공지능기본법안.hwp');
874
+ expect(detail.attachments[1].fileId).toBe('99002');
875
+ expect(detail.attachments[1].filename).toBe('인공지능기본법안검토보고서.pdf');
876
+ });
877
+ test('parses proposalReason from pre tag (br → newline)', () => {
878
+ const detail = parser.parseDetail(NSM_DETAIL_HTML);
879
+ expect(detail.proposalReason).toBe('인공지능 기술의 발전에 따라\n규제 체계를 정비하고자 함');
880
+ });
881
+ test('returns null proposalReason when pre tag is absent', () => {
882
+ const detail = parser.parseDetail(NSM_DETAIL_HTML_NO_REASON);
883
+ expect(detail.proposalReason).toBeNull();
884
+ });
885
+ test('returns empty attachments when no buttons are present', () => {
886
+ const detail = parser.parseDetail(NSM_DETAIL_HTML_NO_REASON);
887
+ expect(detail.attachments).toHaveLength(0);
888
+ });
889
+ test('single-proposer format parses without crash', () => {
890
+ const detail = parser.parseDetail(NSM_DETAIL_HTML_NO_REASON);
891
+ expect(detail.proposer).toBe('이영희의원');
892
+ expect(detail.billNo).toBe('2200001');
893
+ expect(detail.session).toBe('제433회 국회(임시회)');
894
+ });
895
+ });
896
+ });
897
+ // ─── NsmLmSts ─────────────────────────────────────────────────────────────────
898
+ describe('NsmLmSts', () => {
899
+ const nsm = new pal_1.NsmLmSts();
900
+ // ── constructor ────────────────────────────────────────────────────────────
901
+ describe('constructor', () => {
902
+ test('creates instance without config', () => {
903
+ expect(new pal_1.NsmLmSts()).toBeInstanceOf(pal_1.NsmLmSts);
904
+ });
905
+ test('creates instance with empty config object', () => {
906
+ expect(new pal_1.NsmLmSts({})).toBeInstanceOf(pal_1.NsmLmSts);
907
+ });
908
+ test('accepts custom userAgent', () => {
909
+ expect(new pal_1.NsmLmSts({ userAgent: 'Custom Agent' })).toBeInstanceOf(pal_1.NsmLmSts);
910
+ });
911
+ test('accepts custom timeout and retryCount', () => {
912
+ expect(new pal_1.NsmLmSts({ timeout: 5000, retryCount: 2 })).toBeInstanceOf(pal_1.NsmLmSts);
913
+ });
914
+ test('accepts custom headers', () => {
915
+ expect(new pal_1.NsmLmSts({ customHeaders: { 'Accept-Language': 'ko-KR' } })).toBeInstanceOf(pal_1.NsmLmSts);
916
+ });
917
+ });
918
+ // ── parseList / parseDetail (delegation) ──────────────────────────────────
919
+ describe('parseList (delegation)', () => {
920
+ test('delegates to NsmLmStsParser and returns INsmSearchResult', () => {
921
+ const result = nsm.parseList(NSM_LIST_HTML);
922
+ expect(result.total).toBe(3142);
923
+ expect(result.items).toHaveLength(1);
924
+ expect(result.items[0].billName).toBe('인공지능 기본법 일부개정법률안');
925
+ });
926
+ test('returns empty result for empty html', () => {
927
+ const result = nsm.parseList('');
928
+ expect(result.items).toHaveLength(0);
929
+ });
930
+ });
931
+ describe('parseDetail (delegation)', () => {
932
+ test('delegates to NsmLmStsParser and returns INsmBillDetail', () => {
933
+ const detail = nsm.parseDetail(NSM_DETAIL_HTML);
934
+ expect(detail.title).toBe('인공지능 기본법 일부개정법률안');
935
+ expect(detail.billNo).toBe('2219088');
936
+ expect(detail.attachments).toHaveLength(2);
937
+ });
938
+ });
939
+ // ── getDetailHTML validation ───────────────────────────────────────────────
940
+ describe('getDetailHTML input validation', () => {
941
+ test('throws when billNo is empty string', async () => {
942
+ await expect(nsm.getDetailHTML('')).rejects.toThrow('billNo is required');
943
+ });
944
+ test('throws when billNo is whitespace only', async () => {
945
+ await expect(nsm.getDetailHTML(' ')).rejects.toThrow('billNo is required');
946
+ });
947
+ });
948
+ // ── search integration ────────────────────────────────────────────────────
949
+ describe('search (integration)', () => {
950
+ let searchResult;
951
+ beforeAll(async () => {
952
+ searchResult = await nsm.search({ pageSize: 5 });
953
+ debugLog('nsm.search', searchResult);
954
+ });
955
+ test('returns non-garbled html string (getListHTML)', async () => {
956
+ const html = await nsm.getListHTML({ pageSize: 5 });
957
+ expect(typeof html).toBe('string');
958
+ expect(html.length).toBeGreaterThan(0);
959
+ expect(html).not.toContain('\ufffd');
960
+ });
961
+ test('returns INsmSearchResult with total > 0', () => {
962
+ expect(searchResult.total).toBeGreaterThan(0);
963
+ expect(searchResult.totalPages).toBeGreaterThan(0);
964
+ expect(searchResult.currentPage).toBe(1);
965
+ });
966
+ test('returns items with valid structure and no garbled text', () => {
967
+ expect(searchResult.items.length).toBeGreaterThan(0);
968
+ for (const item of searchResult.items) {
969
+ expect(item.billName).not.toContain('\ufffd');
970
+ expect(item.billNo).toMatch(/^\d+$/);
971
+ expect(item.link).toContain('/gcom/nsmLmSts/out/');
972
+ expect(item.proposer).not.toContain('\ufffd');
973
+ expect(item.proposer.length).toBeGreaterThan(0);
974
+ expect(item.proposalDate.length).toBeGreaterThan(0);
975
+ expect(item.progressStatus.length).toBeGreaterThan(0);
976
+ }
977
+ });
978
+ test('pageSize filter limits returned items', async () => {
979
+ const r5 = await nsm.search({ pageSize: 5 });
980
+ const r10 = await nsm.search({ pageSize: 10 });
981
+ expect(r5.items.length).toBeLessThanOrEqual(5);
982
+ expect(r10.items.length).toBeLessThanOrEqual(10);
983
+ });
984
+ test('pageIndex=2 returns different items from page 1', async () => {
985
+ const page1 = await nsm.search({ pageSize: 5, pageIndex: 1 });
986
+ const page2 = await nsm.search({ pageSize: 5, pageIndex: 2 });
987
+ expect(page1.items.length).toBeGreaterThan(0);
988
+ expect(page2.items.length).toBeGreaterThan(0);
989
+ expect(page1.items[0].billNo).not.toBe(page2.items[0].billNo);
990
+ });
991
+ });
992
+ // ── searchPending integration ─────────────────────────────────────────────
993
+ describe('searchPending (integration)', () => {
994
+ let pendingResult;
995
+ beforeAll(async () => {
996
+ pendingResult = await nsm.searchPending({ pageSize: 5 });
997
+ debugLog('nsm.searchPending', pendingResult);
998
+ });
999
+ test('returns INsmSearchResult with valid structure', () => {
1000
+ expect(typeof pendingResult.total).toBe('number');
1001
+ expect(typeof pendingResult.totalPages).toBe('number');
1002
+ expect(pendingResult.currentPage).toBe(1);
1003
+ });
1004
+ test('all returned bills have progressStatus "발의"', () => {
1005
+ for (const item of pendingResult.items) {
1006
+ expect(item.progressStatus).toBe('발의');
1007
+ }
1008
+ });
1009
+ test('all pending bills have no resolutionStatus', () => {
1010
+ for (const item of pendingResult.items) {
1011
+ expect(item.resolutionStatus).toBe('');
1012
+ }
1013
+ });
1014
+ test('no garbled text in pending bill names', () => {
1015
+ for (const item of pendingResult.items) {
1016
+ expect(item.billName).not.toContain('\ufffd');
1017
+ }
1018
+ });
1019
+ });
1020
+ // ── getPage / getDetail integration ───────────────────────────────────────
1021
+ describe('getPage / getDetail (integration)', () => {
1022
+ let firstBillNo;
1023
+ let pageItems;
1024
+ beforeAll(async () => {
1025
+ pageItems = await nsm.getPage(1, { pageSize: 5 });
1026
+ firstBillNo = pageItems[0]?.billNo ?? '';
1027
+ debugLog('nsm.getPage(1)', pageItems);
1028
+ });
1029
+ test('getPage(1): returns INsmBillItem array', () => {
1030
+ expect(Array.isArray(pageItems)).toBe(true);
1031
+ expect(pageItems.length).toBeGreaterThan(0);
1032
+ });
1033
+ test('getPage(2): returns different items from page 1', async () => {
1034
+ const page2 = await nsm.getPage(2, { pageSize: 5 });
1035
+ expect(page2.length).toBeGreaterThan(0);
1036
+ expect(page2[0].billNo).not.toBe(pageItems[0].billNo);
1037
+ });
1038
+ test('getDetailHTML: returns non-empty, non-garbled html', async () => {
1039
+ if (!firstBillNo)
1040
+ return;
1041
+ const html = await nsm.getDetailHTML(firstBillNo);
1042
+ expect(typeof html).toBe('string');
1043
+ expect(html.length).toBeGreaterThan(0);
1044
+ expect(html).not.toContain('\ufffd');
1045
+ });
1046
+ test('getDetail: returns INsmBillDetail with valid fields', async () => {
1047
+ if (!firstBillNo)
1048
+ return;
1049
+ const detail = await nsm.getDetail(firstBillNo);
1050
+ debugLog('nsm.getDetail', detail);
1051
+ expect(detail.title.length).toBeGreaterThan(0);
1052
+ expect(detail.title).not.toContain('\ufffd');
1053
+ expect(detail.billNo).toMatch(/^\d+$/);
1054
+ expect(detail.proposer.length).toBeGreaterThan(0);
1055
+ expect(detail.proposalDate.length).toBeGreaterThan(0);
1056
+ });
1057
+ });
1058
+ // ── getAllPages / getAllPendingPages integration ───────────────────────────
1059
+ describe('getAllPages / getAllPendingPages (integration)', () => {
1060
+ test('getAllPages: yields exactly maxPages results', async () => {
1061
+ const pages = [];
1062
+ for await (const page of nsm.getAllPages({ pageSize: 5 }, { maxPages: 2, delayMs: 0 })) {
1063
+ pages.push(page);
1064
+ }
1065
+ expect(pages).toHaveLength(2);
1066
+ expect(pages[0].items.length).toBeGreaterThan(0);
1067
+ expect(pages[1].items.length).toBeGreaterThan(0);
1068
+ });
1069
+ test('getAllPages: currentPage increments correctly', async () => {
1070
+ const pages = [];
1071
+ for await (const page of nsm.getAllPages({ pageSize: 5 }, { maxPages: 3, delayMs: 0 })) {
1072
+ pages.push(page);
1073
+ }
1074
+ expect(pages[0].currentPage).toBe(1);
1075
+ expect(pages[1].currentPage).toBe(2);
1076
+ expect(pages[2].currentPage).toBe(3);
1077
+ });
1078
+ test('getAllPages: total is consistent across pages', async () => {
1079
+ const pages = [];
1080
+ for await (const page of nsm.getAllPages({ pageSize: 5 }, { maxPages: 2, delayMs: 0 })) {
1081
+ pages.push(page);
1082
+ }
1083
+ expect(pages[0].total).toBe(pages[1].total);
1084
+ expect(pages[0].totalPages).toBe(pages[1].totalPages);
1085
+ });
1086
+ test('getAllPages: page 2 items differ from page 1', async () => {
1087
+ const pages = [];
1088
+ for await (const page of nsm.getAllPages({ pageSize: 5 }, { maxPages: 2, delayMs: 0 })) {
1089
+ pages.push(page);
1090
+ }
1091
+ expect(pages[0].items[0].billNo).not.toBe(pages[1].items[0].billNo);
1092
+ });
1093
+ test('getAllPages: concurrency > 1 yields pages in order', async () => {
1094
+ const pages = [];
1095
+ for await (const page of nsm.getAllPages({ pageSize: 5 }, { maxPages: 4, delayMs: 0, concurrency: 2 })) {
1096
+ pages.push(page);
1097
+ }
1098
+ expect(pages).toHaveLength(4);
1099
+ for (let i = 0; i < pages.length; i++) {
1100
+ expect(pages[i].currentPage).toBe(i + 1);
1101
+ }
1102
+ });
1103
+ test('getAllPendingPages: yields at least 1 page of pending bills', async () => {
1104
+ const pages = [];
1105
+ for await (const page of nsm.getAllPendingPages({ pageSize: 5 }, { maxPages: 1, delayMs: 0 })) {
1106
+ pages.push(page);
1107
+ }
1108
+ expect(pages.length).toBeGreaterThanOrEqual(1);
1109
+ // Every item must be in 발의 상태
1110
+ for (const page of pages) {
1111
+ for (const item of page.items) {
1112
+ expect(item.progressStatus).toBe('발의');
1113
+ }
1114
+ }
1115
+ });
1116
+ });
1117
+ // ── Screenshot functionality ──────────────────────────────────────────────
1118
+ describe('Screenshot functionality', () => {
1119
+ test('creates instance with screenshot config', () => {
1120
+ const instance = new pal_1.NsmLmSts({
1121
+ screenshot: {
1122
+ enabled: true,
1123
+ fullPage: true,
1124
+ width: 1280,
1125
+ height: 800,
1126
+ format: 'png',
1127
+ },
1128
+ });
1129
+ expect(instance).toBeInstanceOf(pal_1.NsmLmSts);
1130
+ });
1131
+ test('throws error when screenshot is disabled', async () => {
1132
+ const instance = new pal_1.NsmLmSts({ screenshot: { enabled: false } });
1133
+ await expect(instance.takeScreenshot('https://example.com')).rejects.toThrow('Screenshot feature is not enabled');
1134
+ });
1135
+ test('accepts jpeg format with quality option', () => {
1136
+ const instance = new pal_1.NsmLmSts({
1137
+ screenshot: { enabled: true, format: 'jpeg', quality: 85 },
1138
+ });
1139
+ expect(instance).toBeInstanceOf(pal_1.NsmLmSts);
1140
+ });
1141
+ test('updateScreenshotConfig modifies screenshot settings', () => {
1142
+ const instance = new pal_1.NsmLmSts({
1143
+ screenshot: { enabled: true, format: 'png' },
1144
+ });
1145
+ instance.updateScreenshotConfig({ format: 'jpeg', quality: 70 });
1146
+ expect(instance).toBeInstanceOf(pal_1.NsmLmSts);
1147
+ });
1148
+ test('initBrowser and closeBrowser can be called', async () => {
1149
+ const instance = new pal_1.NsmLmSts({ screenshot: { enabled: true } });
1150
+ expect(instance).toBeInstanceOf(pal_1.NsmLmSts);
1151
+ });
1152
+ });
1153
+ });
635
1154
  //# sourceMappingURL=pal.test.js.map