@letsrunit/playwright 0.21.1 → 0.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -27,10 +27,196 @@ async function browse(browser, options = {}) {
27
27
  return await context.newPage();
28
28
  }
29
29
 
30
+ // src/fallback-locator.ts
31
+ var FALLBACK_LOCATOR_CANDIDATES = /* @__PURE__ */ Symbol("letsrunit.playwright.fallback-locator-candidates");
32
+ var ACTION_METHODS = /* @__PURE__ */ new Set([
33
+ "blur",
34
+ "check",
35
+ "clear",
36
+ "click",
37
+ "dblclick",
38
+ "dispatchEvent",
39
+ "dragTo",
40
+ "fill",
41
+ "focus",
42
+ "hover",
43
+ "press",
44
+ "pressSequentially",
45
+ "scrollIntoViewIfNeeded",
46
+ "selectOption",
47
+ "setChecked",
48
+ "setInputFiles",
49
+ "tap",
50
+ "type",
51
+ "uncheck"
52
+ ]);
53
+ var LOCATOR_CHAIN_METHODS = /* @__PURE__ */ new Set([
54
+ "and",
55
+ "first",
56
+ "last",
57
+ "locator",
58
+ "nth",
59
+ "or"
60
+ ]);
61
+ var NO_WAIT_OPTION_INDEX = {
62
+ dragTo: 1,
63
+ fill: 1,
64
+ press: 1,
65
+ pressSequentially: 1,
66
+ selectOption: 1,
67
+ setInputFiles: 1,
68
+ type: 1
69
+ };
70
+ function buildOrLocator(candidates) {
71
+ let result = candidates[0];
72
+ for (const candidate of candidates.slice(1)) {
73
+ result = result.or(candidate);
74
+ }
75
+ return result;
76
+ }
77
+ async function firstPresentFallback(candidates) {
78
+ for (const candidate of candidates.slice(1)) {
79
+ try {
80
+ if (await candidate.count() > 0) return candidate;
81
+ } catch {
82
+ }
83
+ }
84
+ return null;
85
+ }
86
+ function withNoWaitTimeout(method, args) {
87
+ const next = [...args];
88
+ const optionIndex = NO_WAIT_OPTION_INDEX[method] ?? 0;
89
+ const current = next[optionIndex];
90
+ if (current && typeof current === "object") {
91
+ next[optionIndex] = { ...current, timeout: 0 };
92
+ } else {
93
+ next[optionIndex] = { timeout: 0 };
94
+ }
95
+ return next;
96
+ }
97
+ function handleExpect(primary, candidates) {
98
+ return (expression, options) => {
99
+ if (expression.includes("to.be.visible") || expression.includes("to.be.attached")) {
100
+ return buildOrLocator(candidates)._expect(expression, options);
101
+ }
102
+ return primary._expect(expression, options);
103
+ };
104
+ }
105
+ function handleAll(candidates) {
106
+ return async () => {
107
+ const all = [];
108
+ for (const candidate of candidates) {
109
+ all.push(...await candidate.all());
110
+ }
111
+ return all;
112
+ };
113
+ }
114
+ function handleLocatorChain(prop, candidates) {
115
+ return (...args) => createFallbackLocator(
116
+ candidates.map((candidate) => {
117
+ const method = candidate[prop];
118
+ return method.apply(candidate, args);
119
+ })
120
+ );
121
+ }
122
+ function handleCount(primary, candidates) {
123
+ return async () => {
124
+ const primaryCount = await primary.count();
125
+ if (primaryCount > 0) return primaryCount;
126
+ const fallback = await firstPresentFallback(candidates);
127
+ if (!fallback) return primaryCount;
128
+ return fallback.count();
129
+ };
130
+ }
131
+ function handleAction(prop, primary, candidates, primaryMethod) {
132
+ return async (...args) => {
133
+ try {
134
+ return await primaryMethod.apply(primary, args);
135
+ } catch (error) {
136
+ const fallback = await firstPresentFallback(candidates);
137
+ if (!fallback) throw error;
138
+ const fallbackMethod = fallback[prop];
139
+ return fallbackMethod.apply(fallback, withNoWaitTimeout(prop, args));
140
+ }
141
+ };
142
+ }
143
+ function handleAsyncFallback(prop, primaryMethod, primary, candidates) {
144
+ return (...args) => {
145
+ const result = primaryMethod.apply(primary, args);
146
+ if (!result || typeof result !== "object" || typeof result.catch !== "function") {
147
+ return result;
148
+ }
149
+ return result.catch(async (error) => {
150
+ const fallback = await firstPresentFallback(candidates);
151
+ if (!fallback) throw error;
152
+ const fallbackMethod = fallback[prop];
153
+ return fallbackMethod.apply(fallback, args);
154
+ });
155
+ };
156
+ }
157
+ function formatLocatorChain(candidates) {
158
+ const parts = candidates.map((candidate) => candidate.toString());
159
+ if (parts.length === 0) return "";
160
+ if (parts.length === 1) return parts[0];
161
+ return `${parts[0]} {fuzzy}`;
162
+ }
163
+ function createFallbackLocator(candidates) {
164
+ const primary = candidates[0];
165
+ const unsupported = /* @__PURE__ */ new Set(["filter", "getByRole", "getByLabel"]);
166
+ const passthroughMetaProperties = /* @__PURE__ */ new Set(["constructor", "__proto__"]);
167
+ const proxy = new Proxy(primary, {
168
+ get(_target, prop) {
169
+ if (prop === FALLBACK_LOCATOR_CANDIDATES) return candidates;
170
+ if (typeof prop !== "string") return primary[prop];
171
+ if (passthroughMetaProperties.has(prop)) return primary[prop];
172
+ switch (prop) {
173
+ case "toString":
174
+ return () => formatLocatorChain(candidates);
175
+ case "all":
176
+ return handleAll(candidates);
177
+ case "_expect":
178
+ return handleExpect(primary, candidates);
179
+ case "count":
180
+ return handleCount(primary, candidates);
181
+ }
182
+ if (unsupported.has(prop)) {
183
+ return () => {
184
+ throw new Error(`FallbackLocator does not support ${prop}`);
185
+ };
186
+ }
187
+ if (LOCATOR_CHAIN_METHODS.has(prop)) {
188
+ return handleLocatorChain(prop, candidates);
189
+ }
190
+ const primaryMethod = primary[prop];
191
+ if (typeof primaryMethod !== "function") return primaryMethod;
192
+ if (ACTION_METHODS.has(prop)) {
193
+ return handleAction(prop, primary, candidates, primaryMethod);
194
+ }
195
+ return handleAsyncFallback(prop, primaryMethod, primary, candidates);
196
+ }
197
+ });
198
+ return proxy;
199
+ }
200
+ function getFallbackLocatorCandidates(locator) {
201
+ return locator[FALLBACK_LOCATOR_CANDIDATES] ?? null;
202
+ }
203
+
30
204
  // src/utils/pick-field-element.ts
205
+ async function resolveConcreteFieldLocator(elements) {
206
+ const fallbackCandidates = getFallbackLocatorCandidates(elements);
207
+ if (!fallbackCandidates) return elements;
208
+ for (const candidate of fallbackCandidates) {
209
+ try {
210
+ if (await candidate.count() > 0) return candidate;
211
+ } catch {
212
+ }
213
+ }
214
+ return elements;
215
+ }
31
216
  async function pickFieldElement(elements) {
217
+ elements = await resolveConcreteFieldLocator(elements);
32
218
  const count = await elements.count();
33
- if (count === 1) return elements;
219
+ if (count <= 1) return elements.first();
34
220
  const candidates = [];
35
221
  for (let i = 0; i < count; i++) {
36
222
  const el = elements.nth(i);
@@ -60,7 +246,7 @@ async function pickFieldElement(elements) {
60
246
  if (isParent !== null) {
61
247
  return elements.nth(isParent);
62
248
  }
63
- return elements;
249
+ return elements.first();
64
250
  }
65
251
 
66
252
  // src/field/aria-select.ts
@@ -1744,9 +1930,7 @@ async function setFieldValue(el, value, options) {
1744
1930
  // fallback (eg contenteditable or will fail)
1745
1931
  setFallback
1746
1932
  );
1747
- if (await el.count() > 1) {
1748
- el = await pickFieldElement(el);
1749
- }
1933
+ el = await pickFieldElement(el);
1750
1934
  const tag = await el.evaluate((e) => e.tagName.toLowerCase(), options);
1751
1935
  const type = (await el.getAttribute("type", options).catch(
1752
1936
  /* v8 ignore next — attribute might be missing or element might have detached during the check */
@@ -1761,175 +1945,6 @@ async function formatHtml(page) {
1761
1945
  return String(file);
1762
1946
  }
1763
1947
 
1764
- // src/fallback-locator.ts
1765
- var ACTION_METHODS = /* @__PURE__ */ new Set([
1766
- "blur",
1767
- "check",
1768
- "clear",
1769
- "click",
1770
- "dblclick",
1771
- "dispatchEvent",
1772
- "dragTo",
1773
- "fill",
1774
- "focus",
1775
- "hover",
1776
- "press",
1777
- "pressSequentially",
1778
- "scrollIntoViewIfNeeded",
1779
- "selectOption",
1780
- "setChecked",
1781
- "setInputFiles",
1782
- "tap",
1783
- "type",
1784
- "uncheck"
1785
- ]);
1786
- var LOCATOR_CHAIN_METHODS = /* @__PURE__ */ new Set([
1787
- "and",
1788
- "first",
1789
- "last",
1790
- "locator",
1791
- "nth",
1792
- "or"
1793
- ]);
1794
- var NO_WAIT_OPTION_INDEX = {
1795
- dragTo: 1,
1796
- fill: 1,
1797
- press: 1,
1798
- pressSequentially: 1,
1799
- selectOption: 1,
1800
- setInputFiles: 1,
1801
- type: 1
1802
- };
1803
- function buildOrLocator(candidates) {
1804
- let result = candidates[0];
1805
- for (const candidate of candidates.slice(1)) {
1806
- result = result.or(candidate);
1807
- }
1808
- return result;
1809
- }
1810
- async function firstPresentFallback(candidates) {
1811
- for (const candidate of candidates.slice(1)) {
1812
- try {
1813
- if (await candidate.count() > 0) return candidate;
1814
- } catch {
1815
- }
1816
- }
1817
- return null;
1818
- }
1819
- function withNoWaitTimeout(method, args) {
1820
- const next = [...args];
1821
- const optionIndex = NO_WAIT_OPTION_INDEX[method] ?? 0;
1822
- const current = next[optionIndex];
1823
- if (current && typeof current === "object") {
1824
- next[optionIndex] = { ...current, timeout: 0 };
1825
- } else {
1826
- next[optionIndex] = { timeout: 0 };
1827
- }
1828
- return next;
1829
- }
1830
- function handleExpect(primary, candidates) {
1831
- return (expression, options) => {
1832
- if (expression.includes("to.be.visible") || expression.includes("to.be.attached")) {
1833
- return buildOrLocator(candidates)._expect(expression, options);
1834
- }
1835
- return primary._expect(expression, options);
1836
- };
1837
- }
1838
- function handleAll(candidates) {
1839
- return async () => {
1840
- const all = [];
1841
- for (const candidate of candidates) {
1842
- all.push(...await candidate.all());
1843
- }
1844
- return all;
1845
- };
1846
- }
1847
- function handleLocatorChain(prop, candidates) {
1848
- return (...args) => createFallbackLocator(
1849
- candidates.map((candidate) => {
1850
- const method = candidate[prop];
1851
- return method.apply(candidate, args);
1852
- })
1853
- );
1854
- }
1855
- function handleCount(primary, candidates) {
1856
- return async () => {
1857
- const primaryCount = await primary.count();
1858
- if (primaryCount > 0) return primaryCount;
1859
- const fallback = await firstPresentFallback(candidates);
1860
- if (!fallback) return primaryCount;
1861
- return fallback.count();
1862
- };
1863
- }
1864
- function handleAction(prop, primary, candidates, primaryMethod) {
1865
- return async (...args) => {
1866
- try {
1867
- return await primaryMethod.apply(primary, args);
1868
- } catch (error) {
1869
- const fallback = await firstPresentFallback(candidates);
1870
- if (!fallback) throw error;
1871
- const fallbackMethod = fallback[prop];
1872
- return fallbackMethod.apply(fallback, withNoWaitTimeout(prop, args));
1873
- }
1874
- };
1875
- }
1876
- function handleAsyncFallback(prop, primaryMethod, primary, candidates) {
1877
- return (...args) => {
1878
- const result = primaryMethod.apply(primary, args);
1879
- if (!result || typeof result !== "object" || typeof result.catch !== "function") {
1880
- return result;
1881
- }
1882
- return result.catch(async (error) => {
1883
- const fallback = await firstPresentFallback(candidates);
1884
- if (!fallback) throw error;
1885
- const fallbackMethod = fallback[prop];
1886
- return fallbackMethod.apply(fallback, args);
1887
- });
1888
- };
1889
- }
1890
- function formatLocatorChain(candidates) {
1891
- const parts = candidates.map((candidate) => candidate.toString());
1892
- if (parts.length === 0) return "";
1893
- if (parts.length === 1) return parts[0];
1894
- return `${parts[0]} {fuzzy}`;
1895
- }
1896
- function createFallbackLocator(candidates) {
1897
- const primary = candidates[0];
1898
- const unsupported = /* @__PURE__ */ new Set(["filter", "getByRole", "getByLabel"]);
1899
- const passthroughMetaProperties = /* @__PURE__ */ new Set(["constructor", "__proto__"]);
1900
- const proxy = new Proxy(primary, {
1901
- get(_target, prop) {
1902
- if (typeof prop !== "string") return primary[prop];
1903
- if (passthroughMetaProperties.has(prop)) return primary[prop];
1904
- switch (prop) {
1905
- case "toString":
1906
- return () => formatLocatorChain(candidates);
1907
- case "all":
1908
- return handleAll(candidates);
1909
- case "_expect":
1910
- return handleExpect(primary, candidates);
1911
- case "count":
1912
- return handleCount(primary, candidates);
1913
- }
1914
- if (unsupported.has(prop)) {
1915
- return () => {
1916
- throw new Error(`FallbackLocator does not support ${prop}`);
1917
- };
1918
- }
1919
- if (LOCATOR_CHAIN_METHODS.has(prop)) {
1920
- return handleLocatorChain(prop, candidates);
1921
- }
1922
- const primaryMethod = primary[prop];
1923
- if (typeof primaryMethod !== "function") return primaryMethod;
1924
- if (ACTION_METHODS.has(prop)) {
1925
- return handleAction(prop, primary, candidates, primaryMethod);
1926
- }
1927
- return handleAsyncFallback(prop, primaryMethod, primary, candidates);
1928
- }
1929
- });
1930
- return proxy;
1931
- }
1932
-
1933
1948
  // src/fuzzy-locator.ts
1934
1949
  function debug(...args) {
1935
1950
  if (process.env.LETSRUNIT_DEBUG_FUZZY_LOCATOR === "1") {