@letsrunit/playwright 0.19.0 → 0.19.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -16,7 +16,7 @@ declare function setFieldValue(el: Locator, value: Value, options?: SetOptions):
16
16
  declare function formatHtml(page: string | Page): Promise<string>;
17
17
 
18
18
  /**
19
- * Locates an element using Playwright selectors, with lazy fallbacks.
19
+ * Locates an element using Playwright selectors, with ordered runtime fallbacks.
20
20
  */
21
21
  declare function fuzzyLocator(page: Page, selector: string): Promise<Locator>;
22
22
 
package/dist/index.js CHANGED
@@ -68746,6 +68746,173 @@ async function formatHtml(page) {
68746
68746
  return String(file);
68747
68747
  }
68748
68748
 
68749
+ // src/fallback-locator.ts
68750
+ var ACTION_METHODS = /* @__PURE__ */ new Set([
68751
+ "blur",
68752
+ "check",
68753
+ "clear",
68754
+ "click",
68755
+ "dblclick",
68756
+ "dispatchEvent",
68757
+ "dragTo",
68758
+ "fill",
68759
+ "focus",
68760
+ "hover",
68761
+ "press",
68762
+ "pressSequentially",
68763
+ "scrollIntoViewIfNeeded",
68764
+ "selectOption",
68765
+ "setChecked",
68766
+ "setInputFiles",
68767
+ "tap",
68768
+ "type",
68769
+ "uncheck"
68770
+ ]);
68771
+ var LOCATOR_CHAIN_METHODS = /* @__PURE__ */ new Set([
68772
+ "and",
68773
+ "first",
68774
+ "last",
68775
+ "locator",
68776
+ "nth",
68777
+ "or"
68778
+ ]);
68779
+ var NO_WAIT_OPTION_INDEX = {
68780
+ dragTo: 1,
68781
+ fill: 1,
68782
+ press: 1,
68783
+ pressSequentially: 1,
68784
+ selectOption: 1,
68785
+ setInputFiles: 1,
68786
+ type: 1
68787
+ };
68788
+ function buildOrLocator(candidates) {
68789
+ let result = candidates[0];
68790
+ for (const candidate of candidates.slice(1)) {
68791
+ result = result.or(candidate);
68792
+ }
68793
+ return result;
68794
+ }
68795
+ async function firstPresentFallback(candidates) {
68796
+ for (const candidate of candidates.slice(1)) {
68797
+ try {
68798
+ if (await candidate.count() > 0) return candidate;
68799
+ } catch {
68800
+ }
68801
+ }
68802
+ return null;
68803
+ }
68804
+ function withNoWaitTimeout(method, args) {
68805
+ const next = [...args];
68806
+ const optionIndex = NO_WAIT_OPTION_INDEX[method] ?? 0;
68807
+ const current = next[optionIndex];
68808
+ if (current && typeof current === "object") {
68809
+ next[optionIndex] = { ...current, timeout: 0 };
68810
+ } else {
68811
+ next[optionIndex] = { timeout: 0 };
68812
+ }
68813
+ return next;
68814
+ }
68815
+ function handleExpect(primary, candidates) {
68816
+ return (expression, options) => {
68817
+ if (expression.includes("to.be.visible") || expression.includes("to.be.attached")) {
68818
+ return buildOrLocator(candidates)._expect(expression, options);
68819
+ }
68820
+ return primary._expect(expression, options);
68821
+ };
68822
+ }
68823
+ function handleAll(candidates) {
68824
+ return async () => {
68825
+ const all = [];
68826
+ for (const candidate of candidates) {
68827
+ all.push(...await candidate.all());
68828
+ }
68829
+ return all;
68830
+ };
68831
+ }
68832
+ function handleLocatorChain(prop, candidates) {
68833
+ return (...args) => createFallbackLocator(
68834
+ candidates.map((candidate) => {
68835
+ const method = candidate[prop];
68836
+ return method.apply(candidate, args);
68837
+ })
68838
+ );
68839
+ }
68840
+ function handleCount(primary, candidates) {
68841
+ return async () => {
68842
+ const primaryCount = await primary.count();
68843
+ if (primaryCount > 0) return primaryCount;
68844
+ const fallback = await firstPresentFallback(candidates);
68845
+ if (!fallback) return primaryCount;
68846
+ return fallback.count();
68847
+ };
68848
+ }
68849
+ function handleAction(prop, primary, candidates, primaryMethod) {
68850
+ return async (...args) => {
68851
+ try {
68852
+ return await primaryMethod.apply(primary, args);
68853
+ } catch (error) {
68854
+ const fallback = await firstPresentFallback(candidates);
68855
+ if (!fallback) throw error;
68856
+ const fallbackMethod = fallback[prop];
68857
+ return fallbackMethod.apply(fallback, withNoWaitTimeout(prop, args));
68858
+ }
68859
+ };
68860
+ }
68861
+ function handleAsyncFallback(prop, primaryMethod, primary, candidates) {
68862
+ return (...args) => {
68863
+ const result = primaryMethod.apply(primary, args);
68864
+ if (!result || typeof result !== "object" || typeof result.catch !== "function") {
68865
+ return result;
68866
+ }
68867
+ return result.catch(async (error) => {
68868
+ const fallback = await firstPresentFallback(candidates);
68869
+ if (!fallback) throw error;
68870
+ const fallbackMethod = fallback[prop];
68871
+ return fallbackMethod.apply(fallback, args);
68872
+ });
68873
+ };
68874
+ }
68875
+ function formatLocatorChain(candidates) {
68876
+ const parts = candidates.map((candidate) => candidate.toString());
68877
+ if (parts.length === 0) return "";
68878
+ if (parts.length === 1) return parts[0];
68879
+ return `${parts[0]} {fuzzy}`;
68880
+ }
68881
+ function createFallbackLocator(candidates) {
68882
+ const primary = candidates[0];
68883
+ const unsupported = /* @__PURE__ */ new Set(["filter", "getByRole", "getByLabel"]);
68884
+ const proxy = new Proxy(primary, {
68885
+ get(_target, prop) {
68886
+ if (typeof prop !== "string") return primary[prop];
68887
+ switch (prop) {
68888
+ case "toString":
68889
+ return () => formatLocatorChain(candidates);
68890
+ case "all":
68891
+ return handleAll(candidates);
68892
+ case "_expect":
68893
+ return handleExpect(primary, candidates);
68894
+ case "count":
68895
+ return handleCount(primary, candidates);
68896
+ }
68897
+ if (unsupported.has(prop)) {
68898
+ return () => {
68899
+ throw new Error(`FallbackLocator does not support ${prop}`);
68900
+ };
68901
+ }
68902
+ if (LOCATOR_CHAIN_METHODS.has(prop)) {
68903
+ return handleLocatorChain(prop, candidates);
68904
+ }
68905
+ const primaryMethod = primary[prop];
68906
+ if (typeof primaryMethod !== "function") return primaryMethod;
68907
+ if (ACTION_METHODS.has(prop)) {
68908
+ return handleAction(prop, primary, candidates, primaryMethod);
68909
+ }
68910
+ return handleAsyncFallback(prop, primaryMethod, primary, candidates);
68911
+ }
68912
+ });
68913
+ return proxy;
68914
+ }
68915
+
68749
68916
  // src/fuzzy-locator.ts
68750
68917
  function debug(...args) {
68751
68918
  if (process.env.LETSRUNIT_DEBUG_FUZZY_LOCATOR === "1") {
@@ -68754,6 +68921,11 @@ function debug(...args) {
68754
68921
  }
68755
68922
  async function fuzzyLocator(page, selector) {
68756
68923
  debug("input selector:", selector);
68924
+ const candidates = buildFuzzyCandidates(page, selector);
68925
+ debug("enabled fallbacks:", candidates.length > 1 ? String(candidates.length - 1) : "(none)");
68926
+ return createFallbackLocator(candidates);
68927
+ }
68928
+ function buildFuzzyCandidates(page, selector) {
68757
68929
  const primary = page.locator(selector);
68758
68930
  const candidates = [
68759
68931
  { name: "relaxNameToHasText", locator: tryRelaxNameToHasText(page, selector) },
@@ -68762,17 +68934,13 @@ async function fuzzyLocator(page, selector) {
68762
68934
  { name: "fieldAlternative", locator: tryFieldAlternative(page, selector) },
68763
68935
  { name: "asField", locator: tryAsField(page, selector) }
68764
68936
  ];
68765
- let combined = primary;
68766
- const enabled = [];
68937
+ const all = [primary];
68767
68938
  for (const candidate of candidates) {
68768
68939
  if (!candidate.locator) continue;
68769
- enabled.push(candidate.name);
68770
- combined = combined.or(candidate.locator);
68940
+ debug("enabling fallback:", candidate.name, candidate.locator.toString());
68941
+ all.push(candidate.locator);
68771
68942
  }
68772
- debug("enabled fallbacks:", enabled.length ? enabled.join(", ") : "(none)");
68773
- const result = combined.first();
68774
- debug("returning locator:", result.toString());
68775
- return result;
68943
+ return all;
68776
68944
  }
68777
68945
  function tryRelaxNameToHasText(page, selector) {
68778
68946
  const matchAnyNameFull = selector.match(/^(role=.*)\[name="([^"]+)"i?](.*)$/i);
@@ -68793,7 +68961,7 @@ function tryRoleNameProximity(page, selector) {
68793
68961
  const matchRole = selector.match(/^role=(\w+)\s*\[name="([^"]+)"i?](.*)$/i);
68794
68962
  if (!matchRole) return null;
68795
68963
  const [, role, name, rest] = matchRole;
68796
- const proximitySelector = `text=${name} >> .. >> role=${role}${rest}`;
68964
+ const proximitySelector = `text=${name} >> xpath=following-sibling::* >> role=${role}${rest}`;
68797
68965
  return page.locator(proximitySelector);
68798
68966
  }
68799
68967
  function tryFieldAlternative(page, selector) {