@hira-core/sdk 1.0.5 → 1.0.7

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.
Files changed (3) hide show
  1. package/dist/index.d.ts +327 -29
  2. package/dist/index.js +540 -133
  3. package/package.json +1 -5
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import * as _packages_shared from '@packages/shared';
2
2
  import { ProfileOutputValue, IWorkerLogMessage, ProfileStatus, IWorkerOutputMessage } from '@packages/shared';
3
- import * as puppeteer from 'puppeteer-core';
3
+ import * as puppeteer_core from 'puppeteer-core';
4
4
  import { Browser, Page, Frame, ElementHandle } from 'puppeteer-core';
5
5
  import { EventEmitter } from 'events';
6
6
 
@@ -521,8 +521,8 @@ declare class GpmStandaloneAdapter implements IBrowserAdapter {
521
521
  constructor(logger: ILogger);
522
522
  private getBoundLogger;
523
523
  open(profileName: string, index: number, windowConfig: IBrowserWindowConfig): Promise<{
524
- browser: puppeteer.Browser;
525
- page: puppeteer.Page;
524
+ browser: puppeteer_core.Browser;
525
+ page: puppeteer_core.Page;
526
526
  profile: IAntidetectProfile;
527
527
  }>;
528
528
  close(profileId: string): Promise<void>;
@@ -595,8 +595,8 @@ declare class HidemiumStandaloneAdapter implements IBrowserAdapter {
595
595
  constructor(logger: ILogger);
596
596
  private getBoundLogger;
597
597
  open(profileName: string, index: number, windowConfig: IBrowserWindowConfig): Promise<{
598
- browser: puppeteer.Browser;
599
- page: puppeteer.Page;
598
+ browser: puppeteer_core.Browser;
599
+ page: puppeteer_core.Page;
600
600
  profile: IAntidetectProfile;
601
601
  }>;
602
602
  close(profileId: string): Promise<void>;
@@ -606,72 +606,370 @@ declare class HidemiumStandaloneAdapter implements IBrowserAdapter {
606
606
  declare class BrowserUtils<TConfig extends IFlowConfig = IFlowConfig> {
607
607
  private ctx;
608
608
  private logger;
609
- /** Output definitions từ flow config — dùng để validate writeOutput key */
609
+ /** Output definitions from flow config — used to validate writeOutput keys */
610
610
  private readonly outputDefs;
611
- /** Set chứa các key hợp lệ build 1 lần từ outputDefs */
611
+ /** Valid output keys setbuilt once from outputDefs */
612
612
  private readonly validOutputKeys;
613
+ /** The initial page when flow starts — activeDefault() returns here */
614
+ private readonly defaultPage;
615
+ /** The currently active page all methods operate on — changed via activeTab() */
616
+ private activePage;
617
+ /** Currently active iframe — null means main frame. Changed via activeIframe() */
618
+ private activeFrame;
613
619
  constructor(context: IScriptContext<TConfig>);
620
+ /**
621
+ * CDP trick — Chromium nghĩ tab luôn focused, không bị throttle
622
+ * khi user chuyển sang tab khác hoặc click vào trình duyệt.
623
+ */
624
+ private enableFocusEmulation;
614
625
  private checkAbort;
626
+ /**
627
+ * Pause execution for the given duration.
628
+ *
629
+ * @param ms - Duration in milliseconds
630
+ * @example await utils.sleep(2000); // wait 2 seconds
631
+ */
615
632
  sleep(ms: number): Promise<void>;
633
+ /**
634
+ * Wait for an element to appear and become visible in the DOM.
635
+ * Returns null if not found (soft fail — does NOT throw).
636
+ * Supports CSS selectors and XPath (auto-detected by `//` or `(` prefix).
637
+ *
638
+ * @param selector - CSS selector or XPath expression
639
+ * @param timeout - Max wait time in ms (default: 8000)
640
+ * @param scope - Optional Frame to search within
641
+ * @returns The element handle, or null if not found
642
+ *
643
+ * @example
644
+ * const el = await utils.waitForElement("#my-btn");
645
+ * if (el) await el.click();
646
+ */
616
647
  waitForElement(selector: string, timeout?: number, scope?: Frame): Promise<ElementHandle | null>;
648
+ /**
649
+ * Resolve an element: if waitTimeout > 0, wait for it; otherwise query directly with $().
650
+ */
651
+ private resolveElement;
652
+ /**
653
+ * Click on an element. Scrolls the element into view before clicking.
654
+ * Throws if the element is not found.
655
+ *
656
+ * @param target - CSS selector, XPath, or an existing ElementHandle
657
+ * @param options.delay - Delay in ms before clicking (default: 1000)
658
+ * @param options.waitTimeout - Max wait time for element to appear (default: 2000)
659
+ * @param options.frame - Optional Frame to search within
660
+ * @returns true on success
661
+ *
662
+ * @example
663
+ * await utils.click("#submit-btn");
664
+ * await utils.click("#btn", { delay: 0 }); // click immediately
665
+ * await utils.click("#btn", { waitTimeout: 8000 }); // wait longer
666
+ */
617
667
  click(target: string | ElementHandle, options?: {
618
668
  delay?: number;
619
- timeout?: number;
669
+ waitTimeout?: number;
620
670
  frame?: Frame;
621
671
  }): Promise<boolean>;
672
+ /**
673
+ * Type text into an input element. Throws if the element is not found.
674
+ *
675
+ * @param selector - CSS selector or XPath of the input
676
+ * @param text - The text to type
677
+ * @param options.mode - Typing mode:
678
+ * - `"replace"` (default): Clear existing text, then type new text
679
+ * - `"append"`: Type without clearing — appends to existing text
680
+ * - `"paste"`: Set value directly via JS (fast, no keystroke simulation)
681
+ * @param options.delay - Delay between keystrokes in ms (default: 50). Ignored in paste mode.
682
+ * @param options.waitTimeout - Max wait time for element to appear in ms (default: 0 — instant)
683
+ * @param options.frame - Optional Frame to search within
684
+ * @returns true on success
685
+ *
686
+ * @example
687
+ * await utils.type("#email", "user@example.com"); // replace mode
688
+ * await utils.type("#input", " more text", { mode: "append" }); // append
689
+ * await utils.type("#address", "0x1234...abcd", { mode: "paste" }); // instant paste
690
+ */
622
691
  type(selector: string, text: string, options?: {
692
+ /** Typing mode: replace (default) = clear then type, append = type without clearing, paste = set value directly */
693
+ mode?: "replace" | "append" | "paste";
623
694
  delay?: number;
695
+ waitTimeout?: number;
624
696
  frame?: Frame;
625
697
  }): Promise<boolean>;
626
- getText(selector: string, frame?: Frame): Promise<string | null>;
698
+ /**
699
+ * Get the text content of an element. Returns null if not found (soft fail).
700
+ *
701
+ * @param selector - CSS selector or XPath
702
+ * @param options.waitTimeout - Max wait time for element to appear in ms (default: 0 — instant)
703
+ * @param options.frame - Optional Frame to search within
704
+ * @returns Trimmed text content, or null if element not found
705
+ *
706
+ * @example
707
+ * const price = await utils.getText(".price");
708
+ * const el = await utils.getText(".lazy-el", { waitTimeout: 8000 });
709
+ */
710
+ getText(selector: string, options?: {
711
+ waitTimeout?: number;
712
+ frame?: Frame;
713
+ }): Promise<string | null>;
714
+ /**
715
+ * Check if an element exists and is visible on the page.
716
+ * Returns false if not found (soft fail — does NOT throw).
717
+ *
718
+ * @param selector - CSS selector or XPath
719
+ * @param timeout - Max wait time in ms (default: 4000)
720
+ * @param frame - Optional Frame to search within
721
+ * @returns true if element exists and is visible
722
+ *
723
+ * @example
724
+ * if (await utils.exists("#popup-overlay")) {
725
+ * await utils.click("#close-popup");
726
+ * }
727
+ */
627
728
  exists(selector: string, timeout?: number, frame?: Frame): Promise<boolean>;
729
+ /**
730
+ * Navigate the active page to a URL. Waits until the page fully loads.
731
+ * Throws on navigation failure or timeout.
732
+ *
733
+ * @param url - The URL to navigate to
734
+ * @param options.waitUntil - When to consider navigation complete (default: "load")
735
+ * @returns true on success
736
+ *
737
+ * @example
738
+ * await utils.goto("https://example.com");
739
+ * await utils.goto("https://app.com", { waitUntil: "networkidle0" });
740
+ */
628
741
  goto(url: string, options?: {
629
742
  waitUntil?: "load" | "domcontentloaded" | "networkidle0" | "networkidle2";
630
743
  }): Promise<boolean>;
744
+ /**
745
+ * Wait for the current page to complete a navigation (e.g. after a form submit).
746
+ * Throws on timeout.
747
+ *
748
+ * @param options.timeout - Max wait time in ms (default: 60000)
749
+ * @param options.waitUntil - When to consider navigation complete (default: "load")
750
+ * @returns true on success
751
+ *
752
+ * @example
753
+ * await utils.click("#submit");
754
+ * await utils.waitForNavigation();
755
+ */
631
756
  waitForNavigation(options?: {
632
757
  timeout?: number;
633
758
  waitUntil?: "load" | "domcontentloaded" | "networkidle0" | "networkidle2";
634
759
  }): Promise<boolean>;
760
+ /**
761
+ * Take a screenshot of the active page. Returns null on failure (soft fail).
762
+ *
763
+ * @param path - Optional file path to save the screenshot. If omitted, returns base64 string.
764
+ * @returns Screenshot buffer (if path given) or base64 string, or null on failure
765
+ *
766
+ * @example
767
+ * await utils.screenshot("./debug.png"); // save to file
768
+ * const base64 = await utils.screenshot(); // get base64
769
+ */
635
770
  screenshot(path?: string): Promise<unknown>;
636
- switchToPopup(matcher: string | RegExp, timeout?: number): Promise<puppeteer.Page | null>;
637
- switchToTabIndex(index: number): Promise<puppeteer.Page | null>;
771
+ /**
772
+ * Switch scope into an iframe within the current page.
773
+ * After switching, all methods (click, type, exists...) operate inside the iframe.
774
+ * Use `activeMainFrame()` to exit back to the main page.
775
+ * Throws if the iframe is not found.
776
+ *
777
+ * @param selector - CSS selector or XPath of the iframe element
778
+ * @param options.waitTimeout - Max wait time for iframe element to appear in ms (default: 0 — instant)
779
+ * @returns The Frame handle
780
+ *
781
+ * @example
782
+ * await utils.activeIframe("#my-iframe");
783
+ * await utils.click("#btn-inside-iframe");
784
+ * await utils.activeMainFrame();
785
+ */
786
+ activeIframe(selector: string, options?: {
787
+ waitTimeout?: number;
788
+ }): Promise<Frame | null>;
789
+ /**
790
+ * Exit the current iframe and return to the main frame of the active page.
791
+ * After calling this, all methods operate on the main page (outside any iframe).
792
+ *
793
+ * @example
794
+ * await utils.activeIframe("#my-iframe");
795
+ * await utils.click("#btn-in-iframe");
796
+ * await utils.activeMainFrame(); // back to main page
797
+ */
798
+ activeMainFrame(): Promise<void>;
799
+ private _findNewTab;
800
+ private _findPopup;
801
+ /**
802
+ * Wait for a new tab to appear, but do NOT switch to it.
803
+ * The active page remains unchanged. Throws if no new tab appears before timeout.
804
+ *
805
+ * @param opts.matcher - String or RegExp to match the new tab's title or URL. If omitted, any new tab matches.
806
+ * @param opts.timeout - Max wait time in ms (default: 8000)
807
+ * @returns The new Page handle (without switching)
808
+ *
809
+ * @example
810
+ * await utils.click("#open-link");
811
+ * const page = await utils.waitForNewTab();
812
+ * const page = await utils.waitForNewTab({ timeout: 20000 });
813
+ * const page = await utils.waitForNewTab({ matcher: "Google" });
814
+ */
815
+ waitForNewTab(opts?: {
816
+ matcher?: string | RegExp;
817
+ timeout?: number;
818
+ }): Promise<puppeteer_core.Page | null>;
819
+ /**
820
+ * Switch to a new tab immediately (or wait if timeout is specified).
821
+ * After calling this, all methods operate on the new tab.
822
+ * Use `activeDefault()` to return to the original tab.
823
+ *
824
+ * @param opts.matcher - String or RegExp to match the new tab's title or URL
825
+ * @param opts.timeout - Max wait time in ms (default: 0 — instant, throws if not found)
826
+ * @returns The new Page handle (now active)
827
+ *
828
+ * @example
829
+ * await utils.click("#open-link");
830
+ * await utils.activeNewTab(); // switch immediately
831
+ * await utils.activeNewTab({ timeout: 8000 }); // wait up to 8s
832
+ * await utils.type("#input", "hello"); // types on the new tab
833
+ * await utils.activeDefault(); // back to original tab
834
+ */
835
+ activeNewTab(opts?: {
836
+ matcher?: string | RegExp;
837
+ timeout?: number;
838
+ }): Promise<puppeteer_core.Page | null>;
839
+ /**
840
+ * Wait for a popup/tab matching the given criteria to appear, but do NOT switch to it.
841
+ * Throws if no matching popup appears before timeout.
842
+ *
843
+ * @param opts.matcher - String or RegExp to match title or URL. If omitted, any new popup matches.
844
+ * @param opts.timeout - Max wait time in ms (default: 8000)
845
+ * @returns The popup Page handle (without switching)
846
+ *
847
+ * @example
848
+ * await utils.click("#connect-wallet");
849
+ * const popup = await utils.waitForPopup({ matcher: "MetaMask" });
850
+ */
851
+ waitForPopup(opts?: {
852
+ matcher?: string | RegExp;
853
+ timeout?: number;
854
+ }): Promise<puppeteer_core.Page | null>;
855
+ /**
856
+ * Switch to a popup/tab immediately (or wait if timeout is specified).
857
+ * After calling this, all methods operate on the popup.
858
+ * Use `activeDefault()` to return to the original tab.
859
+ *
860
+ * @param opts.matcher - String or RegExp to match title or URL
861
+ * @param opts.timeout - Max wait time in ms (default: 0 — instant, throws if not found)
862
+ * @returns The popup Page handle (now active)
863
+ *
864
+ * @example
865
+ * await utils.click("#connect-wallet");
866
+ * await utils.activePopup({ matcher: "MetaMask" });
867
+ * await utils.click("#approve"); // clicks on the popup
868
+ * await utils.activeDefault(); // back to original tab
869
+ */
870
+ activePopup(opts?: {
871
+ matcher?: string | RegExp;
872
+ timeout?: number;
873
+ }): Promise<puppeteer_core.Page | null>;
874
+ /**
875
+ * Switch focus to a tab by its index (0-based, ordered by creation time).
876
+ * All subsequent methods (click, type, goto...) will operate on this tab.
877
+ * User interactions (opening tabs, clicking browser) do NOT affect the active tab.
878
+ * Throws if the index is out of range.
879
+ *
880
+ * @param index - Zero-based tab index
881
+ * @returns The Page handle of the activated tab
882
+ *
883
+ * @example
884
+ * await utils.activeTab(1); // switch to second tab
885
+ * await utils.click("#btn"); // clicks on tab 1
886
+ * await utils.activeTab(0); // back to first tab
887
+ */
888
+ activeTab(index: number): Promise<puppeteer_core.Page | null>;
889
+ /**
890
+ * Close the currently active tab and switch back to the default page.
891
+ *
892
+ * @example
893
+ * await utils.activeTab(1);
894
+ * await utils.closeCurrentTab(); // closes tab 1, returns to tab 0
895
+ */
638
896
  closeCurrentTab(): Promise<void>;
897
+ /**
898
+ * Close all tabs except the currently active one.
899
+ *
900
+ * @example
901
+ * await utils.closeOtherTabs(); // keeps only the active tab open
902
+ */
639
903
  closeOtherTabs(): Promise<void>;
640
904
  /**
641
- * Close ALL tabs (including the current one).
642
- * Useful for full cleanup before flow ends.
905
+ * Close ALL tabs including the current one.
906
+ * Useful for full cleanup before a flow ends.
907
+ *
908
+ * @example
909
+ * await utils.closeAllTabs();
643
910
  */
644
911
  closeAllTabs(): Promise<void>;
645
912
  /**
646
- * Switch back to the default (initial) page — the page stored in context.
647
- * Typically used after switchToPopup() to return to the main tab.
913
+ * Switch back to the default (initial) tab — the first tab opened when the flow started.
914
+ * Resets both the active page and active frame (exits any iframe).
915
+ * Typically used after `activePopup()` or `activeTab()` to return to the main tab.
916
+ *
917
+ * @returns The default Page handle
918
+ *
919
+ * @example
920
+ * await utils.activePopup({ matcher: "MetaMask" });
921
+ * await utils.click("#approve");
922
+ * await utils.activeDefault(); // back to original tab
923
+ */
924
+ activeDefault(): Promise<puppeteer_core.Page>;
925
+ /**
926
+ * Log a config object in a readable format.
927
+ *
928
+ * @param config - Key-value object to log
929
+ * @param label - Label for the log entry (default: "Config")
648
930
  */
649
- switchToDefault(): Promise<puppeteer.Page>;
650
931
  logConfig(config: Record<string, unknown>, label?: string): Promise<void>;
932
+ /** Log the current profile input values for debugging. */
651
933
  logProfileInput(): Promise<void>;
652
934
  /**
653
- * Log toàn bộ output hiện tại (bao gồm cả giá trị từ lần chạy trước và giá trị mới ghi).
654
- * Dùng để debugxem giá trị output hiện tại.
935
+ * Log all current output values (including values from previous runs and newly written values).
936
+ * Useful for debuggingsee what outputs have been set.
655
937
  */
656
938
  logProfileOutput(): Promise<void>;
939
+ /** Log the current global input values for debugging. */
657
940
  logGlobalInput(): Promise<void>;
658
941
  /**
659
- * Ghi kết quả tự do cho profile đang chạy.
660
- * Gửi qua kênh riêng (type: "profile_output") — không lẫn log.
661
- * Đồng thời log ra console/UI để flow dev dễ debug.
942
+ * Write an output value for the current profile.
943
+ * Dispatched via a dedicated channel (type: "profile_output") — separate from logs.
944
+ * Also logs the value to console/UI for debugging.
945
+ *
946
+ * ⚠️ Key must be defined in `config.output[]` — throws Error if invalid.
947
+ *
948
+ * Accepted value types:
949
+ * - `string | number | boolean`
950
+ * - `Array` (max 20 elements, each must be primitive)
951
+ * - `Object` (max 10 entries, values must be primitive)
662
952
  *
663
- * ⚠️ Key phải được định nghĩa trong config.output[] — nếu không sẽ throw Error.
953
+ * @param key - Output key (must match config.output definition)
954
+ * @param value - The value to write
664
955
  *
665
- * value hợp lệ:
666
- * - string | number | boolean
667
- * - array tối đa 20 phần tử (primitive)
668
- * - object 1 cấp tối đa 10 entry (value phải là primitive)
956
+ * @example
957
+ * await utils.writeOutput("status", "success");
958
+ * await utils.writeOutput("balance", 1234.56);
959
+ * await utils.writeOutput("tokens", ["ETH", "USDT"]);
669
960
  */
670
961
  writeOutput(key: InferOutputKeys<TConfig>, value: ProfileOutputValue): Promise<void>;
671
962
  /**
672
- * Cập nhật lại một field trong profileInput của profile đang chạy.
673
- * key phải field đã được định nghĩa trong profileInput schema.
674
- * Kết quả được gửi về server để update AgentFlowConfig sau khi execution xong.
963
+ * Update a profile input field for the currently running profile.
964
+ * The key must be defined in the profileInput schema.
965
+ * The updated value is sent to the server to persist in AgentFlowConfig after execution.
966
+ *
967
+ * @param key - Profile input key (must match schema definition)
968
+ * @param value - The new value (string, number, or boolean)
969
+ *
970
+ * @example
971
+ * await utils.writeProfileInput("lastLoginDate", "2024-01-15");
972
+ * await utils.writeProfileInput("retryCount", 3);
675
973
  */
676
974
  writeProfileInput(key: InferProfileInputKeys<TConfig>, value: string | number | boolean): Promise<void>;
677
975
  private sanitizeOutputValue;
package/dist/index.js CHANGED
@@ -16631,10 +16631,16 @@ var HidemiumStandaloneAdapter = class {
16631
16631
  profileLogger.info(`\u{1F310} Opening Hidemium profile: ${profileName}`);
16632
16632
  this.pendingProfiles.add(profileName);
16633
16633
  try {
16634
- const profiles = await this.service.getProfiles({
16634
+ let profiles = await this.service.getProfiles({
16635
16635
  search: profileName
16636
- });
16637
- const targetProfile = profiles.find((p) => p.name === profileName);
16636
+ }, true);
16637
+ let targetProfile = profiles.find((p) => p.name === profileName);
16638
+ if (!targetProfile) {
16639
+ profiles = await this.service.getProfiles({
16640
+ search: profileName
16641
+ });
16642
+ targetProfile = profiles.find((p) => p.name === profileName);
16643
+ }
16638
16644
  if (!targetProfile) {
16639
16645
  throw new Error(`Profile not found: ${profileName}`);
16640
16646
  }
@@ -16648,7 +16654,10 @@ var HidemiumStandaloneAdapter = class {
16648
16654
  const command = [
16649
16655
  `--window-position=${x},${y}`,
16650
16656
  `--window-size=${windowConfig.width},${windowConfig.height}`,
16651
- `--force-device-scale-factor=${windowConfig.scale}`
16657
+ `--force-device-scale-factor=${windowConfig.scale}`,
16658
+ `--disable-background-timer-throttling`,
16659
+ `--disable-backgrounding-occluded-windows`,
16660
+ `--disable-renderer-backgrounding`
16652
16661
  ].join(" ");
16653
16662
  try {
16654
16663
  await this.service.stopProfile(targetProfile.uuid);
@@ -16712,8 +16721,14 @@ var HidemiumStandaloneAdapter = class {
16712
16721
  this.logger.log(`\u{1F512} Closing pending profile: ${profileName}`);
16713
16722
  const profiles = await this.service.getProfiles({
16714
16723
  search: profileName
16715
- });
16716
- const target = profiles.find((p) => p.name === profileName);
16724
+ }, true);
16725
+ let target = profiles.find((p) => p.name === profileName);
16726
+ if (!target) {
16727
+ const allProfiles = await this.service.getProfiles({
16728
+ search: profileName
16729
+ });
16730
+ target = allProfiles.find((p) => p.name === profileName);
16731
+ }
16717
16732
  if (target) {
16718
16733
  await this.service.stopProfile(target.uuid);
16719
16734
  }
@@ -16800,22 +16815,61 @@ function getSdkConfig() {
16800
16815
  // src/utils/browser.utils.ts
16801
16816
  var BrowserUtils = class {
16802
16817
  constructor(context) {
16818
+ /** Currently active iframe — null means main frame. Changed via activeIframe() */
16819
+ this.activeFrame = null;
16803
16820
  var _a;
16804
16821
  this.ctx = context;
16805
16822
  this.logger = context.logger;
16806
16823
  this.outputDefs = (_a = context.outputDefinitions) != null ? _a : [];
16807
16824
  this.validOutputKeys = new Set(this.outputDefs.map((d) => d.key));
16825
+ this.defaultPage = context.page;
16826
+ this.activePage = context.page;
16827
+ this.enableFocusEmulation(this.activePage).catch(() => {
16828
+ });
16829
+ }
16830
+ /**
16831
+ * CDP trick — Chromium nghĩ tab luôn focused, không bị throttle
16832
+ * khi user chuyển sang tab khác hoặc click vào trình duyệt.
16833
+ */
16834
+ async enableFocusEmulation(page) {
16835
+ try {
16836
+ const cdp = await page.createCDPSession();
16837
+ await cdp.send("Emulation.setFocusEmulationEnabled", { enabled: true });
16838
+ } catch {
16839
+ }
16808
16840
  }
16809
16841
  checkAbort() {
16810
16842
  const signal = global.__HIRA_ABORT_SIGNAL__;
16811
16843
  if (signal == null ? void 0 : signal.aborted) throw new Error("cancelled");
16812
16844
  }
16845
+ /**
16846
+ * Pause execution for the given duration.
16847
+ *
16848
+ * @param ms - Duration in milliseconds
16849
+ * @example await utils.sleep(2000); // wait 2 seconds
16850
+ */
16813
16851
  async sleep(ms) {
16852
+ this.checkAbort();
16814
16853
  await this.log("debug", `\u23F3 Sleep ${ms}ms`);
16815
16854
  await this.rawSleep(ms);
16816
16855
  }
16817
- async waitForElement(selector, timeout = 1e4, scope) {
16818
- const target = scope != null ? scope : this.ctx.page;
16856
+ /**
16857
+ * Wait for an element to appear and become visible in the DOM.
16858
+ * Returns null if not found (soft fail — does NOT throw).
16859
+ * Supports CSS selectors and XPath (auto-detected by `//` or `(` prefix).
16860
+ *
16861
+ * @param selector - CSS selector or XPath expression
16862
+ * @param timeout - Max wait time in ms (default: 8000)
16863
+ * @param scope - Optional Frame to search within
16864
+ * @returns The element handle, or null if not found
16865
+ *
16866
+ * @example
16867
+ * const el = await utils.waitForElement("#my-btn");
16868
+ * if (el) await el.click();
16869
+ */
16870
+ async waitForElement(selector, timeout = 8e3, scope) {
16871
+ var _a;
16872
+ const target = (_a = scope != null ? scope : this.activeFrame) != null ? _a : this.activePage;
16819
16873
  try {
16820
16874
  this.checkAbort();
16821
16875
  const options = { timeout, visible: true };
@@ -16832,7 +16886,35 @@ var BrowserUtils = class {
16832
16886
  return null;
16833
16887
  }
16834
16888
  }
16889
+ /**
16890
+ * Resolve an element: if waitTimeout > 0, wait for it; otherwise query directly with $().
16891
+ */
16892
+ async resolveElement(selector, waitTimeout, scope) {
16893
+ var _a;
16894
+ if (waitTimeout && waitTimeout > 0) {
16895
+ return this.waitForElement(selector, waitTimeout, scope);
16896
+ }
16897
+ const target = (_a = scope != null ? scope : this.activeFrame) != null ? _a : this.activePage;
16898
+ const resolved = selector.startsWith("//") || selector.startsWith("(") ? `xpath/${selector}` : selector;
16899
+ return target.$(resolved);
16900
+ }
16901
+ /**
16902
+ * Click on an element. Scrolls the element into view before clicking.
16903
+ * Throws if the element is not found.
16904
+ *
16905
+ * @param target - CSS selector, XPath, or an existing ElementHandle
16906
+ * @param options.delay - Delay in ms before clicking (default: 1000)
16907
+ * @param options.waitTimeout - Max wait time for element to appear (default: 2000)
16908
+ * @param options.frame - Optional Frame to search within
16909
+ * @returns true on success
16910
+ *
16911
+ * @example
16912
+ * await utils.click("#submit-btn");
16913
+ * await utils.click("#btn", { delay: 0 }); // click immediately
16914
+ * await utils.click("#btn", { waitTimeout: 8000 }); // wait longer
16915
+ */
16835
16916
  async click(target, options) {
16917
+ var _a, _b;
16836
16918
  const label = typeof target === "string" ? target : "[ElementHandle]";
16837
16919
  try {
16838
16920
  this.checkAbort();
@@ -16841,82 +16923,116 @@ var BrowserUtils = class {
16841
16923
  if (typeof target === "string") {
16842
16924
  element = await this.waitForElement(
16843
16925
  target,
16844
- options == null ? void 0 : options.timeout,
16926
+ (_a = options == null ? void 0 : options.waitTimeout) != null ? _a : 2e3,
16845
16927
  options == null ? void 0 : options.frame
16846
16928
  );
16847
16929
  } else {
16848
16930
  element = target;
16849
16931
  }
16850
16932
  if (!element) {
16851
- await this.log(
16852
- "error",
16853
- `\u274C Click failed \u2014 element not found: ${this.shortSelector(label)}`
16854
- );
16855
- return false;
16933
+ throw new Error(`Click failed \u2014 element not found: ${this.shortSelector(label)}`);
16856
16934
  }
16857
16935
  await element.scrollIntoView();
16858
- if (options == null ? void 0 : options.delay) await this.sleep(options.delay);
16936
+ const clickDelay = (_b = options == null ? void 0 : options.delay) != null ? _b : 1e3;
16937
+ if (clickDelay > 0) await this.sleep(clickDelay);
16859
16938
  await element.click();
16860
16939
  await this.actionDelay();
16861
16940
  return true;
16862
16941
  } catch (error) {
16863
16942
  if (error instanceof Error && error.message === "cancelled") throw error;
16864
16943
  const msg = error instanceof Error ? error.message : String(error);
16865
- await this.log(
16866
- "error",
16867
- `\u274C Click failed: ${this.shortSelector(label)} \u2014 ${msg}`
16868
- );
16869
- return false;
16944
+ throw new Error(`Click failed: ${this.shortSelector(label)} \u2014 ${msg}`);
16870
16945
  }
16871
16946
  }
16947
+ /**
16948
+ * Type text into an input element. Throws if the element is not found.
16949
+ *
16950
+ * @param selector - CSS selector or XPath of the input
16951
+ * @param text - The text to type
16952
+ * @param options.mode - Typing mode:
16953
+ * - `"replace"` (default): Clear existing text, then type new text
16954
+ * - `"append"`: Type without clearing — appends to existing text
16955
+ * - `"paste"`: Set value directly via JS (fast, no keystroke simulation)
16956
+ * @param options.delay - Delay between keystrokes in ms (default: 50). Ignored in paste mode.
16957
+ * @param options.waitTimeout - Max wait time for element to appear in ms (default: 0 — instant)
16958
+ * @param options.frame - Optional Frame to search within
16959
+ * @returns true on success
16960
+ *
16961
+ * @example
16962
+ * await utils.type("#email", "user@example.com"); // replace mode
16963
+ * await utils.type("#input", " more text", { mode: "append" }); // append
16964
+ * await utils.type("#address", "0x1234...abcd", { mode: "paste" }); // instant paste
16965
+ */
16872
16966
  async type(selector, text, options) {
16873
- var _a;
16967
+ var _a, _b, _c, _d;
16968
+ const mode = (_a = options == null ? void 0 : options.mode) != null ? _a : "replace";
16874
16969
  const masked = text.length > 20 ? text.slice(0, 20) + "..." : text;
16875
16970
  try {
16876
16971
  this.checkAbort();
16877
16972
  await this.log(
16878
16973
  "info",
16879
- `\u2328\uFE0F Type "${masked}" \u2192 ${this.shortSelector(selector)}`
16974
+ `\u2328\uFE0F Type [${mode}] "${masked}" \u2192 ${this.shortSelector(selector)}`
16880
16975
  );
16881
- const element = await this.waitForElement(
16976
+ const element = await this.resolveElement(
16882
16977
  selector,
16883
- 1e4,
16978
+ options == null ? void 0 : options.waitTimeout,
16884
16979
  options == null ? void 0 : options.frame
16885
16980
  );
16886
16981
  if (!element) {
16887
- await this.log(
16888
- "error",
16889
- `\u274C Type failed \u2014 element not found: ${this.shortSelector(selector)}`
16890
- );
16891
- return false;
16982
+ throw new Error(`Type failed \u2014 element not found: ${this.shortSelector(selector)}`);
16892
16983
  }
16893
16984
  await element.scrollIntoView();
16894
- await element.click({ clickCount: 3 });
16895
- await element.press("Backspace");
16896
- await element.type(text, { delay: (_a = options == null ? void 0 : options.delay) != null ? _a : 50 });
16985
+ if (mode === "paste") {
16986
+ const target = (_b = this.activeFrame) != null ? _b : this.activePage;
16987
+ await target.evaluate(
16988
+ (el, val) => {
16989
+ el.value = val;
16990
+ el.dispatchEvent(new Event("input", { bubbles: true }));
16991
+ el.dispatchEvent(new Event("change", { bubbles: true }));
16992
+ },
16993
+ element,
16994
+ text
16995
+ );
16996
+ } else if (mode === "append") {
16997
+ await element.click();
16998
+ await element.type(text, { delay: (_c = options == null ? void 0 : options.delay) != null ? _c : 50 });
16999
+ } else {
17000
+ await element.click({ clickCount: 3 });
17001
+ await element.press("Backspace");
17002
+ await element.type(text, { delay: (_d = options == null ? void 0 : options.delay) != null ? _d : 50 });
17003
+ }
16897
17004
  await this.actionDelay();
16898
17005
  return true;
16899
17006
  } catch (error) {
16900
17007
  if (error instanceof Error && error.message === "cancelled") throw error;
16901
17008
  const msg = error instanceof Error ? error.message : String(error);
16902
- await this.log(
16903
- "error",
16904
- `\u274C Type failed: ${this.shortSelector(selector)} \u2014 ${msg}`
16905
- );
16906
- return false;
17009
+ throw new Error(`Type failed: ${this.shortSelector(selector)} \u2014 ${msg}`);
16907
17010
  }
16908
17011
  }
16909
- async getText(selector, frame) {
17012
+ /**
17013
+ * Get the text content of an element. Returns null if not found (soft fail).
17014
+ *
17015
+ * @param selector - CSS selector or XPath
17016
+ * @param options.waitTimeout - Max wait time for element to appear in ms (default: 0 — instant)
17017
+ * @param options.frame - Optional Frame to search within
17018
+ * @returns Trimmed text content, or null if element not found
17019
+ *
17020
+ * @example
17021
+ * const price = await utils.getText(".price");
17022
+ * const el = await utils.getText(".lazy-el", { waitTimeout: 8000 });
17023
+ */
17024
+ async getText(selector, options) {
17025
+ var _a, _b;
16910
17026
  try {
16911
17027
  this.checkAbort();
16912
17028
  await this.log("debug", `\u{1F4C4} getText: ${this.shortSelector(selector)}`);
16913
- const scope = frame != null ? frame : this.ctx.page;
16914
- const element = await this.waitForElement(selector, 1e4, frame);
17029
+ const scope = (_b = (_a = options == null ? void 0 : options.frame) != null ? _a : this.activeFrame) != null ? _b : this.activePage;
17030
+ const element = await this.resolveElement(selector, options == null ? void 0 : options.waitTimeout, options == null ? void 0 : options.frame);
16915
17031
  if (!element) return null;
16916
17032
  const text = await scope.evaluate(
16917
17033
  (el) => {
16918
- var _a;
16919
- return ((_a = el.textContent) == null ? void 0 : _a.trim()) || "";
17034
+ var _a2;
17035
+ return ((_a2 = el.textContent) == null ? void 0 : _a2.trim()) || "";
16920
17036
  },
16921
17037
  element
16922
17038
  );
@@ -16930,158 +17046,432 @@ var BrowserUtils = class {
16930
17046
  return null;
16931
17047
  }
16932
17048
  }
16933
- async exists(selector, timeout = 3e3, frame) {
17049
+ /**
17050
+ * Check if an element exists and is visible on the page.
17051
+ * Returns false if not found (soft fail — does NOT throw).
17052
+ *
17053
+ * @param selector - CSS selector or XPath
17054
+ * @param timeout - Max wait time in ms (default: 4000)
17055
+ * @param frame - Optional Frame to search within
17056
+ * @returns true if element exists and is visible
17057
+ *
17058
+ * @example
17059
+ * if (await utils.exists("#popup-overlay")) {
17060
+ * await utils.click("#close-popup");
17061
+ * }
17062
+ */
17063
+ async exists(selector, timeout = 4e3, frame) {
16934
17064
  this.checkAbort();
16935
17065
  const el = await this.waitForElement(selector, timeout, frame);
16936
17066
  return el !== null;
16937
17067
  }
17068
+ /**
17069
+ * Navigate the active page to a URL. Waits until the page fully loads.
17070
+ * Throws on navigation failure or timeout.
17071
+ *
17072
+ * @param url - The URL to navigate to
17073
+ * @param options.waitUntil - When to consider navigation complete (default: "load")
17074
+ * @returns true on success
17075
+ *
17076
+ * @example
17077
+ * await utils.goto("https://example.com");
17078
+ * await utils.goto("https://app.com", { waitUntil: "networkidle0" });
17079
+ */
16938
17080
  async goto(url2, options) {
16939
17081
  try {
16940
17082
  this.checkAbort();
16941
17083
  await this.log("info", `\u{1F310} Navigate \u2192 ${url2}`);
16942
- await this.ctx.page.goto(url2, {
16943
- waitUntil: (options == null ? void 0 : options.waitUntil) || "domcontentloaded",
16944
- timeout: 3e4
17084
+ await this.activePage.goto(url2, {
17085
+ waitUntil: (options == null ? void 0 : options.waitUntil) || "load",
17086
+ timeout: 6e4
16945
17087
  });
16946
17088
  await this.actionDelay();
16947
17089
  return true;
16948
17090
  } catch (error) {
16949
17091
  if (error instanceof Error && error.message === "cancelled") throw error;
16950
17092
  const msg = error instanceof Error ? error.message : String(error);
16951
- await this.log("error", `\u274C Navigate failed: ${url2} \u2014 ${msg}`);
16952
- return false;
17093
+ throw new Error(`Navigate failed: ${url2} \u2014 ${msg}`);
16953
17094
  }
16954
17095
  }
17096
+ /**
17097
+ * Wait for the current page to complete a navigation (e.g. after a form submit).
17098
+ * Throws on timeout.
17099
+ *
17100
+ * @param options.timeout - Max wait time in ms (default: 60000)
17101
+ * @param options.waitUntil - When to consider navigation complete (default: "load")
17102
+ * @returns true on success
17103
+ *
17104
+ * @example
17105
+ * await utils.click("#submit");
17106
+ * await utils.waitForNavigation();
17107
+ */
16955
17108
  async waitForNavigation(options) {
16956
17109
  try {
16957
17110
  this.checkAbort();
16958
17111
  await this.log("debug", "\u{1F504} Waiting for navigation...");
16959
- await this.ctx.page.waitForNavigation({
16960
- timeout: (options == null ? void 0 : options.timeout) || 3e4,
16961
- waitUntil: (options == null ? void 0 : options.waitUntil) || "domcontentloaded"
17112
+ await this.activePage.waitForNavigation({
17113
+ timeout: (options == null ? void 0 : options.timeout) || 16e3,
17114
+ waitUntil: (options == null ? void 0 : options.waitUntil) || "load"
16962
17115
  });
16963
17116
  return true;
16964
17117
  } catch (err) {
16965
17118
  if (err instanceof Error && err.message === "cancelled") throw err;
16966
- await this.log("warn", "\u26A0\uFE0F Navigation timeout");
16967
- return false;
17119
+ throw new Error("Navigation timeout");
16968
17120
  }
16969
17121
  }
17122
+ /**
17123
+ * Take a screenshot of the active page. Returns null on failure (soft fail).
17124
+ *
17125
+ * @param path - Optional file path to save the screenshot. If omitted, returns base64 string.
17126
+ * @returns Screenshot buffer (if path given) or base64 string, or null on failure
17127
+ *
17128
+ * @example
17129
+ * await utils.screenshot("./debug.png"); // save to file
17130
+ * const base64 = await utils.screenshot(); // get base64
17131
+ */
16970
17132
  async screenshot(path2) {
16971
17133
  try {
16972
17134
  this.checkAbort();
16973
17135
  await this.log("info", `\u{1F4F8} Screenshot${path2 ? `: ${path2}` : ""}`);
16974
- return path2 ? await this.ctx.page.screenshot({ path: path2 }) : await this.ctx.page.screenshot({ encoding: "base64" });
17136
+ return path2 ? await this.activePage.screenshot({ path: path2 }) : await this.activePage.screenshot({ encoding: "base64" });
16975
17137
  } catch (error) {
17138
+ if (error instanceof Error && error.message === "cancelled") throw error;
16976
17139
  const msg = error instanceof Error ? error.message : String(error);
16977
- await this.log("error", `\u274C Screenshot failed \u2014 ${msg}`);
17140
+ await this.log("warn", `\u26A0\uFE0F Screenshot failed \u2014 ${msg}`);
16978
17141
  return null;
16979
17142
  }
16980
17143
  }
16981
- async switchToPopup(matcher, timeout = 1e4) {
16982
- const label = typeof matcher === "string" ? matcher : matcher.toString();
17144
+ /**
17145
+ * Switch scope into an iframe within the current page.
17146
+ * After switching, all methods (click, type, exists...) operate inside the iframe.
17147
+ * Use `activeMainFrame()` to exit back to the main page.
17148
+ * Throws if the iframe is not found.
17149
+ *
17150
+ * @param selector - CSS selector or XPath of the iframe element
17151
+ * @param options.waitTimeout - Max wait time for iframe element to appear in ms (default: 0 — instant)
17152
+ * @returns The Frame handle
17153
+ *
17154
+ * @example
17155
+ * await utils.activeIframe("#my-iframe");
17156
+ * await utils.click("#btn-inside-iframe");
17157
+ * await utils.activeMainFrame();
17158
+ */
17159
+ async activeIframe(selector, options) {
17160
+ try {
17161
+ this.checkAbort();
17162
+ await this.log("info", `\u{1F5BC}\uFE0F Active iframe: ${this.shortSelector(selector)}`);
17163
+ const resolved = selector.startsWith("//") || selector.startsWith("(") ? `xpath/${selector}` : selector;
17164
+ const el = (options == null ? void 0 : options.waitTimeout) && options.waitTimeout > 0 ? await this.activePage.waitForSelector(resolved, { timeout: options.waitTimeout }) : await this.activePage.$(resolved);
17165
+ if (!el) {
17166
+ throw new Error(`Iframe not found: ${this.shortSelector(selector)}`);
17167
+ }
17168
+ const frame = await el.contentFrame();
17169
+ if (!frame) {
17170
+ throw new Error(`Cannot get contentFrame from: ${this.shortSelector(selector)}`);
17171
+ }
17172
+ this.activeFrame = frame;
17173
+ return frame;
17174
+ } catch (error) {
17175
+ if (error instanceof Error && error.message === "cancelled") throw error;
17176
+ const msg = error instanceof Error ? error.message : String(error);
17177
+ throw new Error(`activeIframe failed: ${this.shortSelector(selector)} \u2014 ${msg}`);
17178
+ }
17179
+ }
17180
+ /**
17181
+ * Exit the current iframe and return to the main frame of the active page.
17182
+ * After calling this, all methods operate on the main page (outside any iframe).
17183
+ *
17184
+ * @example
17185
+ * await utils.activeIframe("#my-iframe");
17186
+ * await utils.click("#btn-in-iframe");
17187
+ * await utils.activeMainFrame(); // back to main page
17188
+ */
17189
+ async activeMainFrame() {
17190
+ this.checkAbort();
17191
+ this.activeFrame = null;
17192
+ await this.log("info", `\u{1F5BC}\uFE0F Switched to main frame`);
17193
+ }
17194
+ // ── Internal: tìm tab mới ─────────────────────────────────
17195
+ async _findNewTab(opts) {
17196
+ const { matcher, timeout = 8e3 } = opts != null ? opts : {};
17197
+ const label = matcher ? typeof matcher === "string" ? matcher : matcher.toString() : "(any)";
16983
17198
  this.checkAbort();
16984
- await this.log("info", `\u{1F504} Switching to popup: ${label}`);
17199
+ await this.log("info", `\u23F3 Waiting for new tab ${label}... (${timeout}ms)`);
17200
+ const isMatch = (text) => {
17201
+ if (!matcher) return true;
17202
+ if (!text) return false;
17203
+ if (typeof matcher === "string") return text.includes(matcher);
17204
+ return matcher.test(text);
17205
+ };
16985
17206
  try {
17207
+ const currentCount = (await this.ctx.browser.pages()).length;
17208
+ const start = Date.now();
17209
+ while (Date.now() - start < timeout) {
17210
+ const pages = await this.ctx.browser.pages();
17211
+ if (pages.length > currentCount) {
17212
+ for (let i = pages.length - 1; i >= currentCount; i--) {
17213
+ const p = pages[i];
17214
+ try {
17215
+ await p.waitForNavigation({
17216
+ waitUntil: "domcontentloaded",
17217
+ timeout: 3e3
17218
+ }).catch(() => {
17219
+ });
17220
+ } catch {
17221
+ }
17222
+ if (!matcher) {
17223
+ await this.log("success", `\u2705 New tab detected \u2014 tab[${i}]`);
17224
+ return p;
17225
+ }
17226
+ try {
17227
+ const title = await p.title();
17228
+ const url2 = p.url();
17229
+ if (isMatch(title) || isMatch(url2)) {
17230
+ await this.log("success", `\u2705 New tab matched: ${label} \u2014 tab[${i}]`);
17231
+ return p;
17232
+ }
17233
+ } catch {
17234
+ }
17235
+ }
17236
+ }
17237
+ await new Promise((r) => setTimeout(r, 300));
17238
+ }
17239
+ throw new Error(`New tab not found: ${label} (timeout: ${timeout}ms)`);
17240
+ } catch (error) {
17241
+ if (error instanceof Error && error.message === "cancelled") throw error;
17242
+ const msg = error instanceof Error ? error.message : String(error);
17243
+ throw new Error(`waitForNewTab failed \u2014 ${msg}`);
17244
+ }
17245
+ }
17246
+ // ── Internal: tìm popup đã có (hoặc chờ xuất hiện) ──────
17247
+ async _findPopup(opts) {
17248
+ const { matcher, timeout = 8e3 } = opts != null ? opts : {};
17249
+ const label = matcher ? typeof matcher === "string" ? matcher : matcher.toString() : "(any new)";
17250
+ this.checkAbort();
17251
+ await this.log("info", `\u23F3 Waiting for popup ${label}... (${timeout}ms)`);
17252
+ const isMatch = (text) => {
17253
+ if (!matcher) return true;
17254
+ if (!text) return false;
17255
+ if (typeof matcher === "string") return text.includes(matcher);
17256
+ return matcher.test(text);
17257
+ };
17258
+ try {
17259
+ const currentPages = new Set((await this.ctx.browser.pages()).map((p) => p));
16986
17260
  const start = Date.now();
16987
- const isMatch = (text) => {
16988
- if (!text) return false;
16989
- if (typeof matcher === "string") return text.includes(matcher);
16990
- return matcher.test(text);
16991
- };
16992
17261
  while (Date.now() - start < timeout) {
16993
17262
  const pages = await this.ctx.browser.pages();
16994
17263
  for (let i = pages.length - 1; i >= 0; i--) {
16995
17264
  const page = pages[i];
16996
- const title = await page.title();
16997
- const url2 = page.url();
16998
- if (isMatch(title) || isMatch(url2)) {
16999
- await page.bringToFront();
17000
- return page;
17265
+ if (!matcher && currentPages.has(page)) continue;
17266
+ try {
17267
+ const title = await page.title();
17268
+ const url2 = page.url();
17269
+ if (isMatch(title) || isMatch(url2)) {
17270
+ await this.log("success", `\u2705 Popup found: ${label} \u2014 tab[${i}]`);
17271
+ return page;
17272
+ }
17273
+ } catch {
17001
17274
  }
17002
17275
  }
17003
17276
  await new Promise((r) => setTimeout(r, 500));
17004
17277
  }
17005
- await this.log(
17006
- "warn",
17007
- `\u26A0\uFE0F Popup not found: ${label} (timeout: ${timeout}ms)`
17008
- );
17009
- return null;
17278
+ throw new Error(`Popup not found: ${label} (timeout: ${timeout}ms)`);
17010
17279
  } catch (error) {
17280
+ if (error instanceof Error && error.message === "cancelled") throw error;
17011
17281
  const msg = error instanceof Error ? error.message : String(error);
17012
- await this.log("error", `\u274C switchToPopup failed: ${label} \u2014 ${msg}`);
17013
- return null;
17282
+ throw new Error(`waitForPopup failed: ${label} \u2014 ${msg}`);
17283
+ }
17284
+ }
17285
+ /**
17286
+ * Wait for a new tab to appear, but do NOT switch to it.
17287
+ * The active page remains unchanged. Throws if no new tab appears before timeout.
17288
+ *
17289
+ * @param opts.matcher - String or RegExp to match the new tab's title or URL. If omitted, any new tab matches.
17290
+ * @param opts.timeout - Max wait time in ms (default: 8000)
17291
+ * @returns The new Page handle (without switching)
17292
+ *
17293
+ * @example
17294
+ * await utils.click("#open-link");
17295
+ * const page = await utils.waitForNewTab();
17296
+ * const page = await utils.waitForNewTab({ timeout: 20000 });
17297
+ * const page = await utils.waitForNewTab({ matcher: "Google" });
17298
+ */
17299
+ async waitForNewTab(opts) {
17300
+ return this._findNewTab(opts);
17301
+ }
17302
+ /**
17303
+ * Switch to a new tab immediately (or wait if timeout is specified).
17304
+ * After calling this, all methods operate on the new tab.
17305
+ * Use `activeDefault()` to return to the original tab.
17306
+ *
17307
+ * @param opts.matcher - String or RegExp to match the new tab's title or URL
17308
+ * @param opts.timeout - Max wait time in ms (default: 0 — instant, throws if not found)
17309
+ * @returns The new Page handle (now active)
17310
+ *
17311
+ * @example
17312
+ * await utils.click("#open-link");
17313
+ * await utils.activeNewTab(); // switch immediately
17314
+ * await utils.activeNewTab({ timeout: 8000 }); // wait up to 8s
17315
+ * await utils.type("#input", "hello"); // types on the new tab
17316
+ * await utils.activeDefault(); // back to original tab
17317
+ */
17318
+ async activeNewTab(opts) {
17319
+ var _a;
17320
+ const page = await this._findNewTab({ ...opts, timeout: (_a = opts == null ? void 0 : opts.timeout) != null ? _a : 0 });
17321
+ if (page) {
17322
+ await page.bringToFront();
17323
+ await this.enableFocusEmulation(page);
17324
+ this.activePage = page;
17325
+ this.activeFrame = null;
17326
+ }
17327
+ return page;
17328
+ }
17329
+ /**
17330
+ * Wait for a popup/tab matching the given criteria to appear, but do NOT switch to it.
17331
+ * Throws if no matching popup appears before timeout.
17332
+ *
17333
+ * @param opts.matcher - String or RegExp to match title or URL. If omitted, any new popup matches.
17334
+ * @param opts.timeout - Max wait time in ms (default: 8000)
17335
+ * @returns The popup Page handle (without switching)
17336
+ *
17337
+ * @example
17338
+ * await utils.click("#connect-wallet");
17339
+ * const popup = await utils.waitForPopup({ matcher: "MetaMask" });
17340
+ */
17341
+ async waitForPopup(opts) {
17342
+ return this._findPopup(opts);
17343
+ }
17344
+ /**
17345
+ * Switch to a popup/tab immediately (or wait if timeout is specified).
17346
+ * After calling this, all methods operate on the popup.
17347
+ * Use `activeDefault()` to return to the original tab.
17348
+ *
17349
+ * @param opts.matcher - String or RegExp to match title or URL
17350
+ * @param opts.timeout - Max wait time in ms (default: 0 — instant, throws if not found)
17351
+ * @returns The popup Page handle (now active)
17352
+ *
17353
+ * @example
17354
+ * await utils.click("#connect-wallet");
17355
+ * await utils.activePopup({ matcher: "MetaMask" });
17356
+ * await utils.click("#approve"); // clicks on the popup
17357
+ * await utils.activeDefault(); // back to original tab
17358
+ */
17359
+ async activePopup(opts) {
17360
+ var _a;
17361
+ const page = await this._findPopup({ ...opts, timeout: (_a = opts == null ? void 0 : opts.timeout) != null ? _a : 0 });
17362
+ if (page) {
17363
+ await page.bringToFront();
17364
+ await this.enableFocusEmulation(page);
17365
+ this.activePage = page;
17366
+ this.activeFrame = null;
17014
17367
  }
17368
+ return page;
17015
17369
  }
17016
- async switchToTabIndex(index) {
17370
+ /**
17371
+ * Switch focus to a tab by its index (0-based, ordered by creation time).
17372
+ * All subsequent methods (click, type, goto...) will operate on this tab.
17373
+ * User interactions (opening tabs, clicking browser) do NOT affect the active tab.
17374
+ * Throws if the index is out of range.
17375
+ *
17376
+ * @param index - Zero-based tab index
17377
+ * @returns The Page handle of the activated tab
17378
+ *
17379
+ * @example
17380
+ * await utils.activeTab(1); // switch to second tab
17381
+ * await utils.click("#btn"); // clicks on tab 1
17382
+ * await utils.activeTab(0); // back to first tab
17383
+ */
17384
+ async activeTab(index) {
17017
17385
  try {
17018
17386
  this.checkAbort();
17019
- await this.log("info", `\u{1F504} Switching to tab[${index}]...`);
17387
+ await this.log("info", `\u{1F504} Active tab[${index}]...`);
17020
17388
  const pages = await this.ctx.browser.pages();
17021
17389
  if (index >= 0 && index < pages.length) {
17022
17390
  const page = pages[index];
17023
17391
  await page.bringToFront();
17392
+ await this.enableFocusEmulation(page);
17393
+ this.activePage = page;
17394
+ this.activeFrame = null;
17024
17395
  return page;
17025
17396
  }
17026
- await this.log(
17027
- "warn",
17028
- `\u26A0\uFE0F Tab[${index}] not found (total: ${pages.length})`
17029
- );
17030
- return null;
17031
- } catch {
17032
- return null;
17397
+ throw new Error(`Tab[${index}] not found (total: ${pages.length})`);
17398
+ } catch (error) {
17399
+ if (error instanceof Error && error.message === "cancelled") throw error;
17400
+ const msg = error instanceof Error ? error.message : String(error);
17401
+ throw new Error(`activeTab failed \u2014 ${msg}`);
17033
17402
  }
17034
17403
  }
17404
+ /**
17405
+ * Close the currently active tab and switch back to the default page.
17406
+ *
17407
+ * @example
17408
+ * await utils.activeTab(1);
17409
+ * await utils.closeCurrentTab(); // closes tab 1, returns to tab 0
17410
+ */
17035
17411
  async closeCurrentTab() {
17036
- try {
17037
- this.checkAbort();
17038
- if (!this.ctx.page.isClosed()) {
17039
- await this.log("info", `\u{1F5D1}\uFE0F Closing tab: ${this.ctx.page.url()}`);
17040
- await this.ctx.page.close();
17041
- }
17042
- } catch {
17412
+ this.checkAbort();
17413
+ if (!this.activePage.isClosed()) {
17414
+ await this.log("info", `\u{1F5D1}\uFE0F Closing tab: ${this.activePage.url()}`);
17415
+ await this.activePage.close();
17416
+ this.activePage = this.defaultPage;
17043
17417
  }
17044
17418
  }
17419
+ /**
17420
+ * Close all tabs except the currently active one.
17421
+ *
17422
+ * @example
17423
+ * await utils.closeOtherTabs(); // keeps only the active tab open
17424
+ */
17045
17425
  async closeOtherTabs() {
17046
- try {
17047
- this.checkAbort();
17048
- const pages = await this.ctx.browser.pages();
17049
- const toClose = pages.filter((p) => p !== this.ctx.page && !p.isClosed());
17050
- await this.log("info", `\u{1F5D1}\uFE0F Closing ${toClose.length} other tab(s)...`);
17051
- await Promise.all(toClose.map((p) => p.close()));
17052
- } catch (error) {
17053
- const msg = error instanceof Error ? error.message : String(error);
17054
- await this.log("error", `\u274C closeOtherTabs failed \u2014 ${msg}`);
17055
- }
17426
+ this.checkAbort();
17427
+ const pages = await this.ctx.browser.pages();
17428
+ const toClose = pages.filter((p) => p !== this.activePage && !p.isClosed());
17429
+ await this.log("info", `\u{1F5D1}\uFE0F Closing ${toClose.length} other tab(s)...`);
17430
+ await Promise.all(toClose.map((p) => p.close()));
17056
17431
  }
17057
17432
  /**
17058
- * Close ALL tabs (including the current one).
17059
- * Useful for full cleanup before flow ends.
17433
+ * Close ALL tabs including the current one.
17434
+ * Useful for full cleanup before a flow ends.
17435
+ *
17436
+ * @example
17437
+ * await utils.closeAllTabs();
17060
17438
  */
17061
17439
  async closeAllTabs() {
17062
- try {
17063
- this.checkAbort();
17064
- const pages = await this.ctx.browser.pages();
17065
- const toClose = pages.filter((p) => !p.isClosed());
17066
- await this.log("info", `\u{1F5D1}\uFE0F Closing all ${toClose.length} tab(s)...`);
17067
- await Promise.all(toClose.map((p) => p.close()));
17068
- } catch (error) {
17069
- const msg = error instanceof Error ? error.message : String(error);
17070
- await this.log("error", `\u274C closeAllTabs failed \u2014 ${msg}`);
17071
- }
17440
+ this.checkAbort();
17441
+ const pages = await this.ctx.browser.pages();
17442
+ const toClose = pages.filter((p) => !p.isClosed());
17443
+ await this.log("info", `\u{1F5D1}\uFE0F Closing all ${toClose.length} tab(s)...`);
17444
+ await Promise.all(toClose.map((p) => p.close()));
17072
17445
  }
17073
17446
  /**
17074
- * Switch back to the default (initial) page — the page stored in context.
17075
- * Typically used after switchToPopup() to return to the main tab.
17447
+ * Switch back to the default (initial) tab — the first tab opened when the flow started.
17448
+ * Resets both the active page and active frame (exits any iframe).
17449
+ * Typically used after `activePopup()` or `activeTab()` to return to the main tab.
17450
+ *
17451
+ * @returns The default Page handle
17452
+ *
17453
+ * @example
17454
+ * await utils.activePopup({ matcher: "MetaMask" });
17455
+ * await utils.click("#approve");
17456
+ * await utils.activeDefault(); // back to original tab
17076
17457
  */
17077
- async switchToDefault() {
17458
+ async activeDefault() {
17078
17459
  this.checkAbort();
17079
- await this.log("info", `\u{1F504} Switching to default page: ${this.ctx.page.url()}`);
17080
- if (!this.ctx.page.isClosed()) {
17081
- await this.ctx.page.bringToFront();
17460
+ await this.log("info", `\u{1F504} Switching to default page...`);
17461
+ if (!this.defaultPage.isClosed()) {
17462
+ await this.defaultPage.bringToFront();
17463
+ await this.enableFocusEmulation(this.defaultPage);
17082
17464
  }
17083
- return this.ctx.page;
17465
+ this.activePage = this.defaultPage;
17466
+ this.activeFrame = null;
17467
+ return this.activePage;
17084
17468
  }
17469
+ /**
17470
+ * Log a config object in a readable format.
17471
+ *
17472
+ * @param config - Key-value object to log
17473
+ * @param label - Label for the log entry (default: "Config")
17474
+ */
17085
17475
  async logConfig(config, label = "Config") {
17086
17476
  const lines = Object.entries(config).map(([k, v]) => ` ${k}: ${JSON.stringify(v)}`).join(",\n");
17087
17477
  await this.log("info", `\u{1F4CB} ${label}:
@@ -17089,6 +17479,7 @@ var BrowserUtils = class {
17089
17479
  ${lines}
17090
17480
  }`);
17091
17481
  }
17482
+ /** Log the current profile input values for debugging. */
17092
17483
  async logProfileInput() {
17093
17484
  await this.logConfig(
17094
17485
  this.ctx.profileInput,
@@ -17096,8 +17487,8 @@ ${lines}
17096
17487
  );
17097
17488
  }
17098
17489
  /**
17099
- * Log toàn bộ output hiện tại (bao gồm cả giá trị từ lần chạy trước và giá trị mới ghi).
17100
- * Dùng để debugxem giá trị output hiện tại.
17490
+ * Log all current output values (including values from previous runs and newly written values).
17491
+ * Useful for debuggingsee what outputs have been set.
17101
17492
  */
17102
17493
  async logProfileOutput() {
17103
17494
  const output = this.ctx.output;
@@ -17108,6 +17499,7 @@ ${lines}
17108
17499
  }
17109
17500
  await this.logConfig(output, "Profile Output");
17110
17501
  }
17502
+ /** Log the current global input values for debugging. */
17111
17503
  async logGlobalInput() {
17112
17504
  await this.logConfig(
17113
17505
  this.ctx.globalInput,
@@ -17115,16 +17507,24 @@ ${lines}
17115
17507
  );
17116
17508
  }
17117
17509
  /**
17118
- * Ghi kết quả tự do cho profile đang chạy.
17119
- * Gửi qua kênh riêng (type: "profile_output") — không lẫn log.
17120
- * Đồng thời log ra console/UI để flow dev dễ debug.
17510
+ * Write an output value for the current profile.
17511
+ * Dispatched via a dedicated channel (type: "profile_output") — separate from logs.
17512
+ * Also logs the value to console/UI for debugging.
17513
+ *
17514
+ * ⚠️ Key must be defined in `config.output[]` — throws Error if invalid.
17121
17515
  *
17122
- * ⚠️ Key phải được định nghĩa trong config.output[] — nếu không sẽ throw Error.
17516
+ * Accepted value types:
17517
+ * - `string | number | boolean`
17518
+ * - `Array` (max 20 elements, each must be primitive)
17519
+ * - `Object` (max 10 entries, values must be primitive)
17123
17520
  *
17124
- * value hợp lệ:
17125
- * - string | number | boolean
17126
- * - array tối đa 20 phần tử (primitive)
17127
- * - object 1 cấp tối đa 10 entry (value phải là primitive)
17521
+ * @param key - Output key (must match config.output definition)
17522
+ * @param value - The value to write
17523
+ *
17524
+ * @example
17525
+ * await utils.writeOutput("status", "success");
17526
+ * await utils.writeOutput("balance", 1234.56);
17527
+ * await utils.writeOutput("tokens", ["ETH", "USDT"]);
17128
17528
  */
17129
17529
  async writeOutput(key, value) {
17130
17530
  this.checkAbort();
@@ -17145,9 +17545,16 @@ ${lines}
17145
17545
  });
17146
17546
  }
17147
17547
  /**
17148
- * Cập nhật lại một field trong profileInput của profile đang chạy.
17149
- * key phải field đã được định nghĩa trong profileInput schema.
17150
- * Kết quả được gửi về server để update AgentFlowConfig sau khi execution xong.
17548
+ * Update a profile input field for the currently running profile.
17549
+ * The key must be defined in the profileInput schema.
17550
+ * The updated value is sent to the server to persist in AgentFlowConfig after execution.
17551
+ *
17552
+ * @param key - Profile input key (must match schema definition)
17553
+ * @param value - The new value (string, number, or boolean)
17554
+ *
17555
+ * @example
17556
+ * await utils.writeProfileInput("lastLoginDate", "2024-01-15");
17557
+ * await utils.writeProfileInput("retryCount", 3);
17151
17558
  */
17152
17559
  async writeProfileInput(key, value) {
17153
17560
  this.checkAbort();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hira-core/sdk",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "SDK for building Hira automation flows with TypeScript",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -26,10 +26,6 @@
26
26
  ],
27
27
  "author": "tttKiet",
28
28
  "homepage": "https://hira-sdk.vercel.app/",
29
- "repository": {
30
- "type": "git",
31
- "url": "https://github.com/tttKiet/hira-automation"
32
- },
33
29
  "license": "ISC",
34
30
  "engines": {
35
31
  "node": ">=18.0.0"