@visa/cli 4.1.0-rc.191 → 4.1.0-rc.192

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.
@@ -121,6 +121,14 @@ export type SubmitApprovedCheckoutOptions = {
121
121
  onChallengeHold?: (signal: string | null) => void;
122
122
  resolveEmailOtp?: OtpResolver;
123
123
  };
124
+ export type SubmitCandidateMeta = {
125
+ label: string;
126
+ ariaHidden: boolean;
127
+ tabIndex: number | null;
128
+ visible: boolean;
129
+ area: number;
130
+ };
131
+ export declare function preferredSubmitIndex(cands: readonly SubmitCandidateMeta[]): number;
124
132
  export declare function snapshotOrigin(rawUrl: string): string;
125
133
  export type PreparedCheckoutSession = {
126
134
  checkout: PreparedCheckout;
@@ -124,17 +124,52 @@ async function fingerprintSubmitTarget(locator, kind, fallbackLabel) {
124
124
  };
125
125
  }, { targetKind: kind, targetFallbackLabel: fallbackLabel });
126
126
  }
127
+ // Shopify checkouts keep INERT duplicates of the pay control in the DOM —
128
+ // aria-hidden="true", tabindex="-1", and/or zero-size. Playwright still reports
129
+ // those as "visible, enabled and stable", so a bare `.first()` resolves to one
130
+ // and then every click is swallowed by whatever paints on top of it
131
+ // (observed live: `<h3 id="billingAddress"> intercepts pointer events`, retried
132
+ // until the 6s timeout, deterministically, on casper.com). Rank the real
133
+ // controls ahead of the inert ones and prefer a pay-labelled control.
134
+ export function preferredSubmitIndex(cands) {
135
+ const usable = cands
136
+ .map((c, i) => ({ c, i }))
137
+ .filter(({ c }) => c.visible && !c.ariaHidden && c.tabIndex !== -1 && c.area > 0);
138
+ if (usable.length === 0)
139
+ return -1;
140
+ const payLike = usable.find(({ c }) => PAY_LABEL.test(c.label));
141
+ return (payLike ?? usable[0]).i;
142
+ }
143
+ const PAY_LABEL = /pay|place order|complete order|submit order|buy now/i;
127
144
  async function findSubmit(page) {
128
- const submitBtn = page.locator('button[type="submit"], input[type="submit"]').first();
129
- if ((await submitBtn.count().catch(() => 0)) > 0) {
130
- const label = ((await submitBtn.textContent().catch(() => '')) || '').trim() ||
131
- (await submitBtn.getAttribute('value').catch(() => '')) ||
132
- 'submit';
133
- return {
134
- desc: `submit button ("${label}")`,
135
- fingerprint: await fingerprintSubmitTarget(submitBtn, 'submit-control', 'submit'),
136
- click: () => submitBtn.click(),
137
- };
145
+ const controls = page.locator('button[type="submit"], input[type="submit"]');
146
+ const handles = await controls.all().catch(() => []);
147
+ if (handles.length > 0) {
148
+ const metas = await Promise.all(handles.map(async (h) => {
149
+ const text = ((await h.textContent().catch(() => '')) || '').trim();
150
+ const value = text || (await h.getAttribute('value').catch(() => '')) || '';
151
+ const ariaHidden = await h.getAttribute('aria-hidden').catch(() => null);
152
+ const tabIndexRaw = await h.getAttribute('tabindex').catch(() => null);
153
+ const visible = await h.isVisible().catch(() => false);
154
+ const box = await h.boundingBox().catch(() => null);
155
+ return {
156
+ label: value,
157
+ ariaHidden: ariaHidden === 'true',
158
+ tabIndex: tabIndexRaw === null ? null : Number(tabIndexRaw),
159
+ visible,
160
+ area: box ? box.width * box.height : 0,
161
+ };
162
+ }));
163
+ const idx = preferredSubmitIndex(metas);
164
+ if (idx >= 0) {
165
+ const chosen = handles[idx];
166
+ const label = metas[idx].label || 'submit';
167
+ return {
168
+ desc: `submit button ("${label}")`,
169
+ fingerprint: await fingerprintSubmitTarget(chosen, 'submit-control', 'submit'),
170
+ click: () => chosen.click(),
171
+ };
172
+ }
138
173
  }
139
174
  const byText = page.getByRole('button', { name: SUBMIT_TEXT }).first();
140
175
  if ((await byText.count().catch(() => 0)) > 0) {