@hira-core/sdk 1.0.6 → 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 +523 -128
  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
@@ -16654,7 +16654,10 @@ var HidemiumStandaloneAdapter = class {
16654
16654
  const command = [
16655
16655
  `--window-position=${x},${y}`,
16656
16656
  `--window-size=${windowConfig.width},${windowConfig.height}`,
16657
- `--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`
16658
16661
  ].join(" ");
16659
16662
  try {
16660
16663
  await this.service.stopProfile(targetProfile.uuid);
@@ -16812,22 +16815,61 @@ function getSdkConfig() {
16812
16815
  // src/utils/browser.utils.ts
16813
16816
  var BrowserUtils = class {
16814
16817
  constructor(context) {
16818
+ /** Currently active iframe — null means main frame. Changed via activeIframe() */
16819
+ this.activeFrame = null;
16815
16820
  var _a;
16816
16821
  this.ctx = context;
16817
16822
  this.logger = context.logger;
16818
16823
  this.outputDefs = (_a = context.outputDefinitions) != null ? _a : [];
16819
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
+ }
16820
16840
  }
16821
16841
  checkAbort() {
16822
16842
  const signal = global.__HIRA_ABORT_SIGNAL__;
16823
16843
  if (signal == null ? void 0 : signal.aborted) throw new Error("cancelled");
16824
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
+ */
16825
16851
  async sleep(ms) {
16852
+ this.checkAbort();
16826
16853
  await this.log("debug", `\u23F3 Sleep ${ms}ms`);
16827
16854
  await this.rawSleep(ms);
16828
16855
  }
16829
- async waitForElement(selector, timeout = 1e4, scope) {
16830
- 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;
16831
16873
  try {
16832
16874
  this.checkAbort();
16833
16875
  const options = { timeout, visible: true };
@@ -16844,7 +16886,35 @@ var BrowserUtils = class {
16844
16886
  return null;
16845
16887
  }
16846
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
+ */
16847
16916
  async click(target, options) {
16917
+ var _a, _b;
16848
16918
  const label = typeof target === "string" ? target : "[ElementHandle]";
16849
16919
  try {
16850
16920
  this.checkAbort();
@@ -16853,82 +16923,116 @@ var BrowserUtils = class {
16853
16923
  if (typeof target === "string") {
16854
16924
  element = await this.waitForElement(
16855
16925
  target,
16856
- options == null ? void 0 : options.timeout,
16926
+ (_a = options == null ? void 0 : options.waitTimeout) != null ? _a : 2e3,
16857
16927
  options == null ? void 0 : options.frame
16858
16928
  );
16859
16929
  } else {
16860
16930
  element = target;
16861
16931
  }
16862
16932
  if (!element) {
16863
- await this.log(
16864
- "error",
16865
- `\u274C Click failed \u2014 element not found: ${this.shortSelector(label)}`
16866
- );
16867
- return false;
16933
+ throw new Error(`Click failed \u2014 element not found: ${this.shortSelector(label)}`);
16868
16934
  }
16869
16935
  await element.scrollIntoView();
16870
- 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);
16871
16938
  await element.click();
16872
16939
  await this.actionDelay();
16873
16940
  return true;
16874
16941
  } catch (error) {
16875
16942
  if (error instanceof Error && error.message === "cancelled") throw error;
16876
16943
  const msg = error instanceof Error ? error.message : String(error);
16877
- await this.log(
16878
- "error",
16879
- `\u274C Click failed: ${this.shortSelector(label)} \u2014 ${msg}`
16880
- );
16881
- return false;
16944
+ throw new Error(`Click failed: ${this.shortSelector(label)} \u2014 ${msg}`);
16882
16945
  }
16883
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
+ */
16884
16966
  async type(selector, text, options) {
16885
- var _a;
16967
+ var _a, _b, _c, _d;
16968
+ const mode = (_a = options == null ? void 0 : options.mode) != null ? _a : "replace";
16886
16969
  const masked = text.length > 20 ? text.slice(0, 20) + "..." : text;
16887
16970
  try {
16888
16971
  this.checkAbort();
16889
16972
  await this.log(
16890
16973
  "info",
16891
- `\u2328\uFE0F Type "${masked}" \u2192 ${this.shortSelector(selector)}`
16974
+ `\u2328\uFE0F Type [${mode}] "${masked}" \u2192 ${this.shortSelector(selector)}`
16892
16975
  );
16893
- const element = await this.waitForElement(
16976
+ const element = await this.resolveElement(
16894
16977
  selector,
16895
- 1e4,
16978
+ options == null ? void 0 : options.waitTimeout,
16896
16979
  options == null ? void 0 : options.frame
16897
16980
  );
16898
16981
  if (!element) {
16899
- await this.log(
16900
- "error",
16901
- `\u274C Type failed \u2014 element not found: ${this.shortSelector(selector)}`
16902
- );
16903
- return false;
16982
+ throw new Error(`Type failed \u2014 element not found: ${this.shortSelector(selector)}`);
16904
16983
  }
16905
16984
  await element.scrollIntoView();
16906
- await element.click({ clickCount: 3 });
16907
- await element.press("Backspace");
16908
- 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
+ }
16909
17004
  await this.actionDelay();
16910
17005
  return true;
16911
17006
  } catch (error) {
16912
17007
  if (error instanceof Error && error.message === "cancelled") throw error;
16913
17008
  const msg = error instanceof Error ? error.message : String(error);
16914
- await this.log(
16915
- "error",
16916
- `\u274C Type failed: ${this.shortSelector(selector)} \u2014 ${msg}`
16917
- );
16918
- return false;
17009
+ throw new Error(`Type failed: ${this.shortSelector(selector)} \u2014 ${msg}`);
16919
17010
  }
16920
17011
  }
16921
- 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;
16922
17026
  try {
16923
17027
  this.checkAbort();
16924
17028
  await this.log("debug", `\u{1F4C4} getText: ${this.shortSelector(selector)}`);
16925
- const scope = frame != null ? frame : this.ctx.page;
16926
- 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);
16927
17031
  if (!element) return null;
16928
17032
  const text = await scope.evaluate(
16929
17033
  (el) => {
16930
- var _a;
16931
- return ((_a = el.textContent) == null ? void 0 : _a.trim()) || "";
17034
+ var _a2;
17035
+ return ((_a2 = el.textContent) == null ? void 0 : _a2.trim()) || "";
16932
17036
  },
16933
17037
  element
16934
17038
  );
@@ -16942,158 +17046,432 @@ var BrowserUtils = class {
16942
17046
  return null;
16943
17047
  }
16944
17048
  }
16945
- 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) {
16946
17064
  this.checkAbort();
16947
17065
  const el = await this.waitForElement(selector, timeout, frame);
16948
17066
  return el !== null;
16949
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
+ */
16950
17080
  async goto(url2, options) {
16951
17081
  try {
16952
17082
  this.checkAbort();
16953
17083
  await this.log("info", `\u{1F310} Navigate \u2192 ${url2}`);
16954
- await this.ctx.page.goto(url2, {
16955
- waitUntil: (options == null ? void 0 : options.waitUntil) || "domcontentloaded",
16956
- timeout: 3e4
17084
+ await this.activePage.goto(url2, {
17085
+ waitUntil: (options == null ? void 0 : options.waitUntil) || "load",
17086
+ timeout: 6e4
16957
17087
  });
16958
17088
  await this.actionDelay();
16959
17089
  return true;
16960
17090
  } catch (error) {
16961
17091
  if (error instanceof Error && error.message === "cancelled") throw error;
16962
17092
  const msg = error instanceof Error ? error.message : String(error);
16963
- await this.log("error", `\u274C Navigate failed: ${url2} \u2014 ${msg}`);
16964
- return false;
17093
+ throw new Error(`Navigate failed: ${url2} \u2014 ${msg}`);
16965
17094
  }
16966
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
+ */
16967
17108
  async waitForNavigation(options) {
16968
17109
  try {
16969
17110
  this.checkAbort();
16970
17111
  await this.log("debug", "\u{1F504} Waiting for navigation...");
16971
- await this.ctx.page.waitForNavigation({
16972
- timeout: (options == null ? void 0 : options.timeout) || 3e4,
16973
- 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"
16974
17115
  });
16975
17116
  return true;
16976
17117
  } catch (err) {
16977
17118
  if (err instanceof Error && err.message === "cancelled") throw err;
16978
- await this.log("warn", "\u26A0\uFE0F Navigation timeout");
16979
- return false;
17119
+ throw new Error("Navigation timeout");
16980
17120
  }
16981
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
+ */
16982
17132
  async screenshot(path2) {
16983
17133
  try {
16984
17134
  this.checkAbort();
16985
17135
  await this.log("info", `\u{1F4F8} Screenshot${path2 ? `: ${path2}` : ""}`);
16986
- 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" });
16987
17137
  } catch (error) {
17138
+ if (error instanceof Error && error.message === "cancelled") throw error;
16988
17139
  const msg = error instanceof Error ? error.message : String(error);
16989
- await this.log("error", `\u274C Screenshot failed \u2014 ${msg}`);
17140
+ await this.log("warn", `\u26A0\uFE0F Screenshot failed \u2014 ${msg}`);
16990
17141
  return null;
16991
17142
  }
16992
17143
  }
16993
- async switchToPopup(matcher, timeout = 1e4) {
16994
- 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)";
16995
17198
  this.checkAbort();
16996
- 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
+ };
16997
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));
16998
17260
  const start = Date.now();
16999
- const isMatch = (text) => {
17000
- if (!text) return false;
17001
- if (typeof matcher === "string") return text.includes(matcher);
17002
- return matcher.test(text);
17003
- };
17004
17261
  while (Date.now() - start < timeout) {
17005
17262
  const pages = await this.ctx.browser.pages();
17006
17263
  for (let i = pages.length - 1; i >= 0; i--) {
17007
17264
  const page = pages[i];
17008
- const title = await page.title();
17009
- const url2 = page.url();
17010
- if (isMatch(title) || isMatch(url2)) {
17011
- await page.bringToFront();
17012
- 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 {
17013
17274
  }
17014
17275
  }
17015
17276
  await new Promise((r) => setTimeout(r, 500));
17016
17277
  }
17017
- await this.log(
17018
- "warn",
17019
- `\u26A0\uFE0F Popup not found: ${label} (timeout: ${timeout}ms)`
17020
- );
17021
- return null;
17278
+ throw new Error(`Popup not found: ${label} (timeout: ${timeout}ms)`);
17022
17279
  } catch (error) {
17280
+ if (error instanceof Error && error.message === "cancelled") throw error;
17023
17281
  const msg = error instanceof Error ? error.message : String(error);
17024
- await this.log("error", `\u274C switchToPopup failed: ${label} \u2014 ${msg}`);
17025
- return null;
17282
+ throw new Error(`waitForPopup failed: ${label} \u2014 ${msg}`);
17026
17283
  }
17027
17284
  }
17028
- async switchToTabIndex(index) {
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;
17367
+ }
17368
+ return page;
17369
+ }
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) {
17029
17385
  try {
17030
17386
  this.checkAbort();
17031
- await this.log("info", `\u{1F504} Switching to tab[${index}]...`);
17387
+ await this.log("info", `\u{1F504} Active tab[${index}]...`);
17032
17388
  const pages = await this.ctx.browser.pages();
17033
17389
  if (index >= 0 && index < pages.length) {
17034
17390
  const page = pages[index];
17035
17391
  await page.bringToFront();
17392
+ await this.enableFocusEmulation(page);
17393
+ this.activePage = page;
17394
+ this.activeFrame = null;
17036
17395
  return page;
17037
17396
  }
17038
- await this.log(
17039
- "warn",
17040
- `\u26A0\uFE0F Tab[${index}] not found (total: ${pages.length})`
17041
- );
17042
- return null;
17043
- } catch {
17044
- 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}`);
17045
17402
  }
17046
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
+ */
17047
17411
  async closeCurrentTab() {
17048
- try {
17049
- this.checkAbort();
17050
- if (!this.ctx.page.isClosed()) {
17051
- await this.log("info", `\u{1F5D1}\uFE0F Closing tab: ${this.ctx.page.url()}`);
17052
- await this.ctx.page.close();
17053
- }
17054
- } 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;
17055
17417
  }
17056
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
+ */
17057
17425
  async closeOtherTabs() {
17058
- try {
17059
- this.checkAbort();
17060
- const pages = await this.ctx.browser.pages();
17061
- const toClose = pages.filter((p) => p !== this.ctx.page && !p.isClosed());
17062
- await this.log("info", `\u{1F5D1}\uFE0F Closing ${toClose.length} other tab(s)...`);
17063
- await Promise.all(toClose.map((p) => p.close()));
17064
- } catch (error) {
17065
- const msg = error instanceof Error ? error.message : String(error);
17066
- await this.log("error", `\u274C closeOtherTabs failed \u2014 ${msg}`);
17067
- }
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()));
17068
17431
  }
17069
17432
  /**
17070
- * Close ALL tabs (including the current one).
17071
- * 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();
17072
17438
  */
17073
17439
  async closeAllTabs() {
17074
- try {
17075
- this.checkAbort();
17076
- const pages = await this.ctx.browser.pages();
17077
- const toClose = pages.filter((p) => !p.isClosed());
17078
- await this.log("info", `\u{1F5D1}\uFE0F Closing all ${toClose.length} tab(s)...`);
17079
- await Promise.all(toClose.map((p) => p.close()));
17080
- } catch (error) {
17081
- const msg = error instanceof Error ? error.message : String(error);
17082
- await this.log("error", `\u274C closeAllTabs failed \u2014 ${msg}`);
17083
- }
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()));
17084
17445
  }
17085
17446
  /**
17086
- * Switch back to the default (initial) page — the page stored in context.
17087
- * 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
17088
17457
  */
17089
- async switchToDefault() {
17458
+ async activeDefault() {
17090
17459
  this.checkAbort();
17091
- await this.log("info", `\u{1F504} Switching to default page: ${this.ctx.page.url()}`);
17092
- if (!this.ctx.page.isClosed()) {
17093
- 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);
17094
17464
  }
17095
- return this.ctx.page;
17465
+ this.activePage = this.defaultPage;
17466
+ this.activeFrame = null;
17467
+ return this.activePage;
17096
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
+ */
17097
17475
  async logConfig(config, label = "Config") {
17098
17476
  const lines = Object.entries(config).map(([k, v]) => ` ${k}: ${JSON.stringify(v)}`).join(",\n");
17099
17477
  await this.log("info", `\u{1F4CB} ${label}:
@@ -17101,6 +17479,7 @@ var BrowserUtils = class {
17101
17479
  ${lines}
17102
17480
  }`);
17103
17481
  }
17482
+ /** Log the current profile input values for debugging. */
17104
17483
  async logProfileInput() {
17105
17484
  await this.logConfig(
17106
17485
  this.ctx.profileInput,
@@ -17108,8 +17487,8 @@ ${lines}
17108
17487
  );
17109
17488
  }
17110
17489
  /**
17111
- * 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).
17112
- * 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.
17113
17492
  */
17114
17493
  async logProfileOutput() {
17115
17494
  const output = this.ctx.output;
@@ -17120,6 +17499,7 @@ ${lines}
17120
17499
  }
17121
17500
  await this.logConfig(output, "Profile Output");
17122
17501
  }
17502
+ /** Log the current global input values for debugging. */
17123
17503
  async logGlobalInput() {
17124
17504
  await this.logConfig(
17125
17505
  this.ctx.globalInput,
@@ -17127,16 +17507,24 @@ ${lines}
17127
17507
  );
17128
17508
  }
17129
17509
  /**
17130
- * Ghi kết quả tự do cho profile đang chạy.
17131
- * Gửi qua kênh riêng (type: "profile_output") — không lẫn log.
17132
- * Đồ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.
17515
+ *
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)
17133
17520
  *
17134
- * ⚠️ Key phải được định nghĩa trong config.output[] — nếu không sẽ throw Error.
17521
+ * @param key - Output key (must match config.output definition)
17522
+ * @param value - The value to write
17135
17523
  *
17136
- * value hợp lệ:
17137
- * - string | number | boolean
17138
- * - array tối đa 20 phần tử (primitive)
17139
- * - object 1 cấp tối đa 10 entry (value phải là primitive)
17524
+ * @example
17525
+ * await utils.writeOutput("status", "success");
17526
+ * await utils.writeOutput("balance", 1234.56);
17527
+ * await utils.writeOutput("tokens", ["ETH", "USDT"]);
17140
17528
  */
17141
17529
  async writeOutput(key, value) {
17142
17530
  this.checkAbort();
@@ -17157,9 +17545,16 @@ ${lines}
17157
17545
  });
17158
17546
  }
17159
17547
  /**
17160
- * Cập nhật lại một field trong profileInput của profile đang chạy.
17161
- * key phải field đã được định nghĩa trong profileInput schema.
17162
- * 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);
17163
17558
  */
17164
17559
  async writeProfileInput(key, value) {
17165
17560
  this.checkAbort();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hira-core/sdk",
3
- "version": "1.0.6",
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"