import { Effect, Schema } from "effect";
import { type BrowserOperationError, browserFailure, browserSync } from "./browser-effects";
export const BROWSER_FILL_FIELDS_MAX_FIELDS = 16;
export const BROWSER_FILL_FIELDS_MAX_TEXT_BYTES = 16 * 1024;
export const BROWSER_FILL_FIELDS_MAX_TIMEOUT_MS = 30_000;
export type BrowserFillFieldsReason =
| "invalid_target"
| "stale_snapshot"
| "target_changed"
| "document_changed"
| "policy_changed"
| "deadline"
| "input_failed"
| "snapshot_failed"
| "tab_closed";
export class BrowserFillFieldsFailure extends Schema.TaggedError<BrowserFillFieldsFailure>()(
"BrowserFillFieldsFailure",
{
reason: Schema.Literals([
"invalid_target",
"stale_snapshot",
"target_changed",
"document_changed",
"policy_changed",
"deadline",
]),
},
) {}
export interface BrowserFillFieldsProgress {
confirmed: number[];
failedIndex: number | null;
uncertainIndex: number | null;
stopped: boolean;
}
export interface BrowserPreparedField {
verify(): Effect.Effect<void, BrowserOperationError>;
replace(onDispatch: () => void): Effect.Effect<void, BrowserOperationError>;
}
export function fillFieldsFailure(reason: BrowserFillFieldsFailure["reason"]): BrowserOperationError {
return browserFailure(new BrowserFillFieldsFailure({ reason }));
}
export const fillPreparedFields = Effect.fn("BrowserFillFields.fillPrepared")(function* (
prepared: readonly BrowserPreparedField[],
progress: BrowserFillFieldsProgress,
deadline: number,
checkPolicy: () => void,
) {
for (const [index, field] of prepared.entries()) {
progress.failedIndex = index;
if (progress.stopped || Date.now() >= deadline) return yield* fillFieldsFailure("deadline");
yield* browserSync(checkPolicy);
yield* field.verify();
yield* field.replace(() => {
progress.uncertainIndex = index;
});
progress.confirmed.push(index);
progress.uncertainIndex = null;
}
progress.failedIndex = null;
});
export const FILL_FIELD_STATE_FUNCTION = String.raw`function(expected, origin, select, text, requireActive) {
if (!this.isConnected || this.ownerDocument !== document || window !== window.top || location.origin !== origin) return null;
if (this.localName !== 'textarea' && (this.localName !== 'input' || !['text', 'search', 'tel', 'url', 'email'].includes(this.type))) return null;
if (this.isContentEditable || this.readOnly || this.disabled || this.matches(':disabled') || this.getAttribute('aria-disabled') === 'true') return null;
const autocomplete = (this.getAttribute('autocomplete') || this.form?.getAttribute('autocomplete') || '').toLowerCase().split(/\s+/);
if (autocomplete.some(value => value.includes('password') || value === 'one-time-code' || value.startsWith('cc-'))) return null;
if (this.getClientRects().length === 0) return null;
for (let element = this; element; element = element.parentElement || element.getRootNode()?.host) {
const style = getComputedStyle(element);
if (Element.prototype.hasAttribute.call(element, 'hidden') || Element.prototype.hasAttribute.call(element, 'inert') || element.getAttribute('aria-hidden') === 'true' || style.display === 'none' || style.visibility === 'hidden' || style.visibility === 'collapse' || style.contentVisibility === 'hidden' || style.opacity === '0') return null;
}
const fingerprint = JSON.stringify([this.localName, this.type || '', this.getAttribute('autocomplete'), this.form?.getAttribute('autocomplete'), this.getAttribute('id'), this.getAttribute('name'), this.getAttribute('aria-label'), this.getAttribute('placeholder'), this.getAttribute('maxlength'), this.getAttribute('pattern')]);
const state = JSON.stringify([fingerprint, this.value]);
if (expected !== null && (text === null ? state !== expected : fingerprint !== JSON.parse(expected)[0])) return null;
if (select || requireActive) {
let active = document.activeElement;
while (active?.shadowRoot?.activeElement) active = active.shadowRoot.activeElement;
if (active !== this) return null;
if (select) this.select();
}
if (text !== null && this.value !== text) return null;
return state;
}`;