Next.js website for Rocky Mountain Vending company featuring: - Product catalog with Stripe integration - Service areas and parts pages - Admin dashboard with Clerk authentication - SEO optimized pages with JSON-LD structured data Co-authored-by: Cursor <cursoragent@cursor.com>
749 lines
No EOL
26 KiB
Text
749 lines
No EOL
26 KiB
Text
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
|
|
if (value !== null && value !== void 0) {
|
|
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
var dispose, inner;
|
|
if (async) {
|
|
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
dispose = value[Symbol.asyncDispose];
|
|
}
|
|
if (dispose === void 0) {
|
|
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
dispose = value[Symbol.dispose];
|
|
if (async) inner = dispose;
|
|
}
|
|
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
|
|
env.stack.push({ value: value, dispose: dispose, async: async });
|
|
}
|
|
else if (async) {
|
|
env.stack.push({ async: true });
|
|
}
|
|
return value;
|
|
};
|
|
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
|
|
return function (env) {
|
|
function fail(e) {
|
|
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
env.hasError = true;
|
|
}
|
|
var r, s = 0;
|
|
function next() {
|
|
while (r = env.stack.pop()) {
|
|
try {
|
|
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
if (r.dispose) {
|
|
var result = r.dispose.call(r.value);
|
|
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
|
|
}
|
|
else s |= 1;
|
|
}
|
|
catch (e) {
|
|
fail(e);
|
|
}
|
|
}
|
|
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
if (env.hasError) throw env.error;
|
|
}
|
|
return next();
|
|
};
|
|
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
var e = new Error(message);
|
|
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
});
|
|
import { EMPTY, catchError, defaultIfEmpty, defer, filter, first, firstValueFrom, from, identity, ignoreElements, map, merge, mergeMap, noop, of, pipe, race, raceWith, retry, tap, throwIfEmpty, } from '../../../third_party/rxjs/rxjs.js';
|
|
import { EventEmitter } from '../../common/EventEmitter.js';
|
|
import { debugError, fromAbortSignal, timeout } from '../../common/util.js';
|
|
/**
|
|
* All the events that a locator instance may emit.
|
|
*
|
|
* @public
|
|
*/
|
|
export var LocatorEvent;
|
|
(function (LocatorEvent) {
|
|
/**
|
|
* Emitted every time before the locator performs an action on the located element(s).
|
|
*/
|
|
LocatorEvent["Action"] = "action";
|
|
})(LocatorEvent || (LocatorEvent = {}));
|
|
/**
|
|
* Locators describe a strategy of locating objects and performing an action on
|
|
* them. If the action fails because the object is not ready for the action, the
|
|
* whole operation is retried. Various preconditions for a successful action are
|
|
* checked automatically.
|
|
*
|
|
* See {@link https://pptr.dev/guides/page-interactions#locators} for details.
|
|
*
|
|
* @public
|
|
*/
|
|
export class Locator extends EventEmitter {
|
|
/**
|
|
* Creates a race between multiple locators trying to locate elements in
|
|
* parallel but ensures that only a single element receives the action.
|
|
*
|
|
* @public
|
|
*/
|
|
static race(locators) {
|
|
return RaceLocator.create(locators);
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
visibility = null;
|
|
/**
|
|
* @internal
|
|
*/
|
|
_timeout = 30000;
|
|
#ensureElementIsInTheViewport = true;
|
|
#waitForEnabled = true;
|
|
#waitForStableBoundingBox = true;
|
|
/**
|
|
* @internal
|
|
*/
|
|
operators = {
|
|
conditions: (conditions, signal) => {
|
|
return mergeMap((handle) => {
|
|
return merge(...conditions.map(condition => {
|
|
return condition(handle, signal);
|
|
})).pipe(defaultIfEmpty(handle));
|
|
});
|
|
},
|
|
retryAndRaceWithSignalAndTimer: (signal, cause) => {
|
|
const candidates = [];
|
|
if (signal) {
|
|
candidates.push(fromAbortSignal(signal, cause));
|
|
}
|
|
candidates.push(timeout(this._timeout, cause));
|
|
return pipe(retry({ delay: RETRY_DELAY }), raceWith(...candidates));
|
|
},
|
|
};
|
|
// Determines when the locator will timeout for actions.
|
|
get timeout() {
|
|
return this._timeout;
|
|
}
|
|
/**
|
|
* Creates a new locator instance by cloning the current locator and setting
|
|
* the total timeout for the locator actions.
|
|
*
|
|
* Pass `0` to disable timeout.
|
|
*
|
|
* @defaultValue `Page.getDefaultTimeout()`
|
|
*/
|
|
setTimeout(timeout) {
|
|
const locator = this._clone();
|
|
locator._timeout = timeout;
|
|
return locator;
|
|
}
|
|
/**
|
|
* Creates a new locator instance by cloning the current locator with the
|
|
* visibility property changed to the specified value.
|
|
*/
|
|
setVisibility(visibility) {
|
|
const locator = this._clone();
|
|
locator.visibility = visibility;
|
|
return locator;
|
|
}
|
|
/**
|
|
* Creates a new locator instance by cloning the current locator and
|
|
* specifying whether to wait for input elements to become enabled before the
|
|
* action. Applicable to `click` and `fill` actions.
|
|
*
|
|
* @defaultValue `true`
|
|
*/
|
|
setWaitForEnabled(value) {
|
|
const locator = this._clone();
|
|
locator.#waitForEnabled = value;
|
|
return locator;
|
|
}
|
|
/**
|
|
* Creates a new locator instance by cloning the current locator and
|
|
* specifying whether the locator should scroll the element into viewport if
|
|
* it is not in the viewport already.
|
|
*
|
|
* @defaultValue `true`
|
|
*/
|
|
setEnsureElementIsInTheViewport(value) {
|
|
const locator = this._clone();
|
|
locator.#ensureElementIsInTheViewport = value;
|
|
return locator;
|
|
}
|
|
/**
|
|
* Creates a new locator instance by cloning the current locator and
|
|
* specifying whether the locator has to wait for the element's bounding box
|
|
* to be same between two consecutive animation frames.
|
|
*
|
|
* @defaultValue `true`
|
|
*/
|
|
setWaitForStableBoundingBox(value) {
|
|
const locator = this._clone();
|
|
locator.#waitForStableBoundingBox = value;
|
|
return locator;
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
copyOptions(locator) {
|
|
this._timeout = locator._timeout;
|
|
this.visibility = locator.visibility;
|
|
this.#waitForEnabled = locator.#waitForEnabled;
|
|
this.#ensureElementIsInTheViewport = locator.#ensureElementIsInTheViewport;
|
|
this.#waitForStableBoundingBox = locator.#waitForStableBoundingBox;
|
|
return this;
|
|
}
|
|
/**
|
|
* If the element has a "disabled" property, wait for the element to be
|
|
* enabled.
|
|
*/
|
|
#waitForEnabledIfNeeded = (handle, signal) => {
|
|
if (!this.#waitForEnabled) {
|
|
return EMPTY;
|
|
}
|
|
return from(handle.frame.waitForFunction(element => {
|
|
if (!(element instanceof HTMLElement)) {
|
|
return true;
|
|
}
|
|
const isNativeFormControl = [
|
|
'BUTTON',
|
|
'INPUT',
|
|
'SELECT',
|
|
'TEXTAREA',
|
|
'OPTION',
|
|
'OPTGROUP',
|
|
].includes(element.nodeName);
|
|
return !isNativeFormControl || !element.hasAttribute('disabled');
|
|
}, {
|
|
timeout: this._timeout,
|
|
signal,
|
|
}, handle)).pipe(ignoreElements());
|
|
};
|
|
/**
|
|
* Compares the bounding box of the element for two consecutive animation
|
|
* frames and waits till they are the same.
|
|
*/
|
|
#waitForStableBoundingBoxIfNeeded = (handle) => {
|
|
if (!this.#waitForStableBoundingBox) {
|
|
return EMPTY;
|
|
}
|
|
return defer(() => {
|
|
// Note we don't use waitForFunction because that relies on RAF.
|
|
return from(handle.evaluate(element => {
|
|
return new Promise(resolve => {
|
|
window.requestAnimationFrame(() => {
|
|
const rect1 = element.getBoundingClientRect();
|
|
window.requestAnimationFrame(() => {
|
|
const rect2 = element.getBoundingClientRect();
|
|
resolve([
|
|
{
|
|
x: rect1.x,
|
|
y: rect1.y,
|
|
width: rect1.width,
|
|
height: rect1.height,
|
|
},
|
|
{
|
|
x: rect2.x,
|
|
y: rect2.y,
|
|
width: rect2.width,
|
|
height: rect2.height,
|
|
},
|
|
]);
|
|
});
|
|
});
|
|
});
|
|
}));
|
|
}).pipe(first(([rect1, rect2]) => {
|
|
return (rect1.x === rect2.x &&
|
|
rect1.y === rect2.y &&
|
|
rect1.width === rect2.width &&
|
|
rect1.height === rect2.height);
|
|
}), retry({ delay: RETRY_DELAY }), ignoreElements());
|
|
};
|
|
/**
|
|
* Checks if the element is in the viewport and auto-scrolls it if it is not.
|
|
*/
|
|
#ensureElementIsInTheViewportIfNeeded = (handle) => {
|
|
if (!this.#ensureElementIsInTheViewport) {
|
|
return EMPTY;
|
|
}
|
|
return from(handle.isIntersectingViewport({ threshold: 0 })).pipe(filter(isIntersectingViewport => {
|
|
return !isIntersectingViewport;
|
|
}), mergeMap(() => {
|
|
return from(handle.scrollIntoView());
|
|
}), mergeMap(() => {
|
|
return defer(() => {
|
|
return from(handle.isIntersectingViewport({ threshold: 0 }));
|
|
}).pipe(first(identity), retry({ delay: RETRY_DELAY }), ignoreElements());
|
|
}));
|
|
};
|
|
#click(options) {
|
|
const signal = options?.signal;
|
|
const cause = new Error('Locator.click');
|
|
return this._wait(options).pipe(this.operators.conditions([
|
|
this.#ensureElementIsInTheViewportIfNeeded,
|
|
this.#waitForStableBoundingBoxIfNeeded,
|
|
this.#waitForEnabledIfNeeded,
|
|
], signal), tap(() => {
|
|
return this.emit(LocatorEvent.Action, undefined);
|
|
}), mergeMap(handle => {
|
|
return from(handle.click(options)).pipe(catchError(err => {
|
|
void handle.dispose().catch(debugError);
|
|
throw err;
|
|
}));
|
|
}), this.operators.retryAndRaceWithSignalAndTimer(signal, cause));
|
|
}
|
|
#fill(value, options) {
|
|
const signal = options?.signal;
|
|
const cause = new Error('Locator.fill');
|
|
return this._wait(options).pipe(this.operators.conditions([
|
|
this.#ensureElementIsInTheViewportIfNeeded,
|
|
this.#waitForStableBoundingBoxIfNeeded,
|
|
this.#waitForEnabledIfNeeded,
|
|
], signal), tap(() => {
|
|
return this.emit(LocatorEvent.Action, undefined);
|
|
}), mergeMap(handle => {
|
|
return from(handle.evaluate(el => {
|
|
if (el instanceof HTMLSelectElement) {
|
|
return 'select';
|
|
}
|
|
if (el instanceof HTMLTextAreaElement) {
|
|
return 'typeable-input';
|
|
}
|
|
if (el instanceof HTMLInputElement) {
|
|
if (new Set([
|
|
'textarea',
|
|
'text',
|
|
'url',
|
|
'tel',
|
|
'search',
|
|
'password',
|
|
'number',
|
|
'email',
|
|
]).has(el.type)) {
|
|
return 'typeable-input';
|
|
}
|
|
else {
|
|
return 'other-input';
|
|
}
|
|
}
|
|
if (el.isContentEditable) {
|
|
return 'contenteditable';
|
|
}
|
|
return 'unknown';
|
|
}))
|
|
.pipe(mergeMap(inputType => {
|
|
switch (inputType) {
|
|
case 'select':
|
|
return from(handle.select(value).then(noop));
|
|
case 'contenteditable':
|
|
case 'typeable-input':
|
|
return from(handle.evaluate((input, newValue) => {
|
|
const currentValue = input.isContentEditable
|
|
? input.innerText
|
|
: input.value;
|
|
// Clear the input if the current value does not match the filled
|
|
// out value.
|
|
if (newValue.length <= currentValue.length ||
|
|
!newValue.startsWith(input.value)) {
|
|
if (input.isContentEditable) {
|
|
input.innerText = '';
|
|
}
|
|
else {
|
|
input.value = '';
|
|
}
|
|
return newValue;
|
|
}
|
|
const originalValue = input.isContentEditable
|
|
? input.innerText
|
|
: input.value;
|
|
// If the value is partially filled out, only type the rest. Move
|
|
// cursor to the end of the common prefix.
|
|
if (input.isContentEditable) {
|
|
input.innerText = '';
|
|
input.innerText = originalValue;
|
|
}
|
|
else {
|
|
input.value = '';
|
|
input.value = originalValue;
|
|
}
|
|
return newValue.substring(originalValue.length);
|
|
}, value)).pipe(mergeMap(textToType => {
|
|
return from(handle.type(textToType));
|
|
}));
|
|
case 'other-input':
|
|
return from(handle.focus()).pipe(mergeMap(() => {
|
|
return from(handle.evaluate((input, value) => {
|
|
input.value = value;
|
|
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
input.dispatchEvent(new Event('change', { bubbles: true }));
|
|
}, value));
|
|
}));
|
|
case 'unknown':
|
|
throw new Error(`Element cannot be filled out.`);
|
|
}
|
|
}))
|
|
.pipe(catchError(err => {
|
|
void handle.dispose().catch(debugError);
|
|
throw err;
|
|
}));
|
|
}), this.operators.retryAndRaceWithSignalAndTimer(signal, cause));
|
|
}
|
|
#hover(options) {
|
|
const signal = options?.signal;
|
|
const cause = new Error('Locator.hover');
|
|
return this._wait(options).pipe(this.operators.conditions([
|
|
this.#ensureElementIsInTheViewportIfNeeded,
|
|
this.#waitForStableBoundingBoxIfNeeded,
|
|
], signal), tap(() => {
|
|
return this.emit(LocatorEvent.Action, undefined);
|
|
}), mergeMap(handle => {
|
|
return from(handle.hover()).pipe(catchError(err => {
|
|
void handle.dispose().catch(debugError);
|
|
throw err;
|
|
}));
|
|
}), this.operators.retryAndRaceWithSignalAndTimer(signal, cause));
|
|
}
|
|
#scroll(options) {
|
|
const signal = options?.signal;
|
|
const cause = new Error('Locator.scroll');
|
|
return this._wait(options).pipe(this.operators.conditions([
|
|
this.#ensureElementIsInTheViewportIfNeeded,
|
|
this.#waitForStableBoundingBoxIfNeeded,
|
|
], signal), tap(() => {
|
|
return this.emit(LocatorEvent.Action, undefined);
|
|
}), mergeMap(handle => {
|
|
return from(handle.evaluate((el, scrollTop, scrollLeft) => {
|
|
if (scrollTop !== undefined) {
|
|
el.scrollTop = scrollTop;
|
|
}
|
|
if (scrollLeft !== undefined) {
|
|
el.scrollLeft = scrollLeft;
|
|
}
|
|
}, options?.scrollTop, options?.scrollLeft)).pipe(catchError(err => {
|
|
void handle.dispose().catch(debugError);
|
|
throw err;
|
|
}));
|
|
}), this.operators.retryAndRaceWithSignalAndTimer(signal, cause));
|
|
}
|
|
/**
|
|
* Clones the locator.
|
|
*/
|
|
clone() {
|
|
return this._clone();
|
|
}
|
|
/**
|
|
* Waits for the locator to get a handle from the page.
|
|
*
|
|
* @public
|
|
*/
|
|
async waitHandle(options) {
|
|
const cause = new Error('Locator.waitHandle');
|
|
return await firstValueFrom(this._wait(options).pipe(this.operators.retryAndRaceWithSignalAndTimer(options?.signal, cause)));
|
|
}
|
|
/**
|
|
* Waits for the locator to get the serialized value from the page.
|
|
*
|
|
* Note this requires the value to be JSON-serializable.
|
|
*
|
|
* @public
|
|
*/
|
|
async wait(options) {
|
|
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
try {
|
|
const handle = __addDisposableResource(env_1, await this.waitHandle(options), false);
|
|
return await handle.jsonValue();
|
|
}
|
|
catch (e_1) {
|
|
env_1.error = e_1;
|
|
env_1.hasError = true;
|
|
}
|
|
finally {
|
|
__disposeResources(env_1);
|
|
}
|
|
}
|
|
/**
|
|
* Maps the locator using the provided mapper.
|
|
*
|
|
* @public
|
|
*/
|
|
map(mapper) {
|
|
return new MappedLocator(this._clone(), handle => {
|
|
// SAFETY: TypeScript cannot deduce the type.
|
|
return handle.evaluateHandle(mapper);
|
|
});
|
|
}
|
|
/**
|
|
* Creates an expectation that is evaluated against located values.
|
|
*
|
|
* If the expectations do not match, then the locator will retry.
|
|
*
|
|
* @public
|
|
*/
|
|
filter(predicate) {
|
|
return new FilteredLocator(this._clone(), async (handle, signal) => {
|
|
await handle.frame.waitForFunction(predicate, { signal, timeout: this._timeout }, handle);
|
|
return true;
|
|
});
|
|
}
|
|
/**
|
|
* Creates an expectation that is evaluated against located handles.
|
|
*
|
|
* If the expectations do not match, then the locator will retry.
|
|
*
|
|
* @internal
|
|
*/
|
|
filterHandle(predicate) {
|
|
return new FilteredLocator(this._clone(), predicate);
|
|
}
|
|
/**
|
|
* Maps the locator using the provided mapper.
|
|
*
|
|
* @internal
|
|
*/
|
|
mapHandle(mapper) {
|
|
return new MappedLocator(this._clone(), mapper);
|
|
}
|
|
/**
|
|
* Clicks the located element.
|
|
*/
|
|
click(options) {
|
|
return firstValueFrom(this.#click(options));
|
|
}
|
|
/**
|
|
* Fills out the input identified by the locator using the provided value. The
|
|
* type of the input is determined at runtime and the appropriate fill-out
|
|
* method is chosen based on the type. `contenteditable`, select, textarea and
|
|
* input elements are supported.
|
|
*/
|
|
fill(value, options) {
|
|
return firstValueFrom(this.#fill(value, options));
|
|
}
|
|
/**
|
|
* Hovers over the located element.
|
|
*/
|
|
hover(options) {
|
|
return firstValueFrom(this.#hover(options));
|
|
}
|
|
/**
|
|
* Scrolls the located element.
|
|
*/
|
|
scroll(options) {
|
|
return firstValueFrom(this.#scroll(options));
|
|
}
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
export class FunctionLocator extends Locator {
|
|
static create(pageOrFrame, func) {
|
|
return new FunctionLocator(pageOrFrame, func).setTimeout('getDefaultTimeout' in pageOrFrame
|
|
? pageOrFrame.getDefaultTimeout()
|
|
: pageOrFrame.page().getDefaultTimeout());
|
|
}
|
|
#pageOrFrame;
|
|
#func;
|
|
constructor(pageOrFrame, func) {
|
|
super();
|
|
this.#pageOrFrame = pageOrFrame;
|
|
this.#func = func;
|
|
}
|
|
_clone() {
|
|
return new FunctionLocator(this.#pageOrFrame, this.#func);
|
|
}
|
|
_wait(options) {
|
|
const signal = options?.signal;
|
|
return defer(() => {
|
|
return from(this.#pageOrFrame.waitForFunction(this.#func, {
|
|
timeout: this.timeout,
|
|
signal,
|
|
}));
|
|
}).pipe(throwIfEmpty());
|
|
}
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
export class DelegatedLocator extends Locator {
|
|
#delegate;
|
|
constructor(delegate) {
|
|
super();
|
|
this.#delegate = delegate;
|
|
this.copyOptions(this.#delegate);
|
|
}
|
|
get delegate() {
|
|
return this.#delegate;
|
|
}
|
|
setTimeout(timeout) {
|
|
const locator = super.setTimeout(timeout);
|
|
locator.#delegate = this.#delegate.setTimeout(timeout);
|
|
return locator;
|
|
}
|
|
setVisibility(visibility) {
|
|
const locator = super.setVisibility(visibility);
|
|
locator.#delegate = locator.#delegate.setVisibility(visibility);
|
|
return locator;
|
|
}
|
|
setWaitForEnabled(value) {
|
|
const locator = super.setWaitForEnabled(value);
|
|
locator.#delegate = this.#delegate.setWaitForEnabled(value);
|
|
return locator;
|
|
}
|
|
setEnsureElementIsInTheViewport(value) {
|
|
const locator = super.setEnsureElementIsInTheViewport(value);
|
|
locator.#delegate = this.#delegate.setEnsureElementIsInTheViewport(value);
|
|
return locator;
|
|
}
|
|
setWaitForStableBoundingBox(value) {
|
|
const locator = super.setWaitForStableBoundingBox(value);
|
|
locator.#delegate = this.#delegate.setWaitForStableBoundingBox(value);
|
|
return locator;
|
|
}
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
export class FilteredLocator extends DelegatedLocator {
|
|
#predicate;
|
|
constructor(base, predicate) {
|
|
super(base);
|
|
this.#predicate = predicate;
|
|
}
|
|
_clone() {
|
|
return new FilteredLocator(this.delegate.clone(), this.#predicate).copyOptions(this);
|
|
}
|
|
_wait(options) {
|
|
return this.delegate._wait(options).pipe(mergeMap(handle => {
|
|
return from(Promise.resolve(this.#predicate(handle, options?.signal))).pipe(filter(value => {
|
|
return value;
|
|
}), map(() => {
|
|
// SAFETY: It passed the predicate, so this is correct.
|
|
return handle;
|
|
}));
|
|
}), throwIfEmpty());
|
|
}
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
export class MappedLocator extends DelegatedLocator {
|
|
#mapper;
|
|
constructor(base, mapper) {
|
|
super(base);
|
|
this.#mapper = mapper;
|
|
}
|
|
_clone() {
|
|
return new MappedLocator(this.delegate.clone(), this.#mapper).copyOptions(this);
|
|
}
|
|
_wait(options) {
|
|
return this.delegate._wait(options).pipe(mergeMap(handle => {
|
|
return from(Promise.resolve(this.#mapper(handle, options?.signal)));
|
|
}));
|
|
}
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
export class NodeLocator extends Locator {
|
|
static create(pageOrFrame, selector) {
|
|
return new NodeLocator(pageOrFrame, selector).setTimeout('getDefaultTimeout' in pageOrFrame
|
|
? pageOrFrame.getDefaultTimeout()
|
|
: pageOrFrame.page().getDefaultTimeout());
|
|
}
|
|
static createFromHandle(pageOrFrame, handle) {
|
|
return new NodeLocator(pageOrFrame, handle).setTimeout('getDefaultTimeout' in pageOrFrame
|
|
? pageOrFrame.getDefaultTimeout()
|
|
: pageOrFrame.page().getDefaultTimeout());
|
|
}
|
|
#pageOrFrame;
|
|
#selectorOrHandle;
|
|
constructor(pageOrFrame, selectorOrHandle) {
|
|
super();
|
|
this.#pageOrFrame = pageOrFrame;
|
|
this.#selectorOrHandle = selectorOrHandle;
|
|
}
|
|
/**
|
|
* Waits for the element to become visible or hidden. visibility === 'visible'
|
|
* means that the element has a computed style, the visibility property other
|
|
* than 'hidden' or 'collapse' and non-empty bounding box. visibility ===
|
|
* 'hidden' means the opposite of that.
|
|
*/
|
|
#waitForVisibilityIfNeeded = (handle) => {
|
|
if (!this.visibility) {
|
|
return EMPTY;
|
|
}
|
|
return (() => {
|
|
switch (this.visibility) {
|
|
case 'hidden':
|
|
return defer(() => {
|
|
return from(handle.isHidden());
|
|
});
|
|
case 'visible':
|
|
return defer(() => {
|
|
return from(handle.isVisible());
|
|
});
|
|
}
|
|
})().pipe(first(identity), retry({ delay: RETRY_DELAY }), ignoreElements());
|
|
};
|
|
_clone() {
|
|
return new NodeLocator(this.#pageOrFrame,
|
|
// @ts-expect-error TSC does cannot parse private overloads.
|
|
this.#selectorOrHandle).copyOptions(this);
|
|
}
|
|
_wait(options) {
|
|
const signal = options?.signal;
|
|
return defer(() => {
|
|
if (typeof this.#selectorOrHandle === 'string') {
|
|
return from(this.#pageOrFrame.waitForSelector(this.#selectorOrHandle, {
|
|
visible: false,
|
|
timeout: this._timeout,
|
|
signal,
|
|
}));
|
|
}
|
|
else {
|
|
return of(this.#selectorOrHandle);
|
|
}
|
|
}).pipe(filter((value) => {
|
|
return value !== null;
|
|
}), throwIfEmpty(), this.operators.conditions([this.#waitForVisibilityIfNeeded], signal));
|
|
}
|
|
}
|
|
function checkLocatorArray(locators) {
|
|
for (const locator of locators) {
|
|
if (!(locator instanceof Locator)) {
|
|
throw new Error('Unknown locator for race candidate');
|
|
}
|
|
}
|
|
return locators;
|
|
}
|
|
/**
|
|
* @internal
|
|
*/
|
|
export class RaceLocator extends Locator {
|
|
static create(locators) {
|
|
const array = checkLocatorArray(locators);
|
|
return new RaceLocator(array);
|
|
}
|
|
#locators;
|
|
constructor(locators) {
|
|
super();
|
|
this.#locators = locators;
|
|
}
|
|
_clone() {
|
|
return new RaceLocator(this.#locators.map(locator => {
|
|
return locator.clone();
|
|
})).copyOptions(this);
|
|
}
|
|
_wait(options) {
|
|
return race(...this.#locators.map(locator => {
|
|
return locator._wait(options);
|
|
}));
|
|
}
|
|
}
|
|
/**
|
|
* For observables coming from promises, a delay is needed, otherwise RxJS will
|
|
* never yield in a permanent failure for a promise.
|
|
*
|
|
* We also don't want RxJS to do promise operations to often, so we bump the
|
|
* delay up to 100ms.
|
|
*
|
|
* @internal
|
|
*/
|
|
export const RETRY_DELAY = 100;
|
|
//# sourceMappingURL=locators.js.map |