chore: migrate expect(locator).toMatchText to protocol (#9117)
This commit is contained in:
parent
d0a4480cf7
commit
0908dc98c8
|
|
@ -212,6 +212,12 @@ export class Locator implements api.Locator {
|
||||||
return this._frame.$$eval(this._selector, ee => ee.map(e => e.textContent || ''));
|
return this._frame.$$eval(this._selector, ee => ee.map(e => e.textContent || ''));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async _expect(expression: string, options: channels.FrameExpectOptions): Promise<{ pass: boolean, received: string }> {
|
||||||
|
return this._frame._wrapApiCall(async (channel: channels.FrameChannel) => {
|
||||||
|
return (await channel.expect({ selector: this._selector, expression, ...options }));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
[(util.inspect as any).custom]() {
|
[(util.inspect as any).custom]() {
|
||||||
return this.toString();
|
return this.toString();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -226,4 +226,8 @@ export class FrameDispatcher extends Dispatcher<Frame, channels.FrameInitializer
|
||||||
async title(params: channels.FrameTitleParams, metadata: CallMetadata): Promise<channels.FrameTitleResult> {
|
async title(params: channels.FrameTitleParams, metadata: CallMetadata): Promise<channels.FrameTitleResult> {
|
||||||
return { value: await this._frame.title() };
|
return { value: await this._frame.title() };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async expect(params: channels.FrameExpectParams, metadata: CallMetadata): Promise<channels.FrameExpectResult> {
|
||||||
|
return await this._frame.expect(metadata, params.selector, params.expression, params);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -71,6 +71,15 @@ export type SerializedArgument = {
|
||||||
handles: Channel[],
|
handles: Channel[],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type ExpectedTextValue = {
|
||||||
|
string?: string,
|
||||||
|
regexSource?: string,
|
||||||
|
regexFlags?: string,
|
||||||
|
matchSubstring?: boolean,
|
||||||
|
normalizeWhiteSpace?: boolean,
|
||||||
|
useInnerText?: boolean,
|
||||||
|
};
|
||||||
|
|
||||||
export type AXNode = {
|
export type AXNode = {
|
||||||
role: string,
|
role: string,
|
||||||
name: string,
|
name: string,
|
||||||
|
|
@ -1609,6 +1618,7 @@ export interface FrameChannel extends Channel {
|
||||||
waitForTimeout(params: FrameWaitForTimeoutParams, metadata?: Metadata): Promise<FrameWaitForTimeoutResult>;
|
waitForTimeout(params: FrameWaitForTimeoutParams, metadata?: Metadata): Promise<FrameWaitForTimeoutResult>;
|
||||||
waitForFunction(params: FrameWaitForFunctionParams, metadata?: Metadata): Promise<FrameWaitForFunctionResult>;
|
waitForFunction(params: FrameWaitForFunctionParams, metadata?: Metadata): Promise<FrameWaitForFunctionResult>;
|
||||||
waitForSelector(params: FrameWaitForSelectorParams, metadata?: Metadata): Promise<FrameWaitForSelectorResult>;
|
waitForSelector(params: FrameWaitForSelectorParams, metadata?: Metadata): Promise<FrameWaitForSelectorResult>;
|
||||||
|
expect(params: FrameExpectParams, metadata?: Metadata): Promise<FrameExpectResult>;
|
||||||
}
|
}
|
||||||
export type FrameLoadstateEvent = {
|
export type FrameLoadstateEvent = {
|
||||||
add?: 'load' | 'domcontentloaded' | 'networkidle',
|
add?: 'load' | 'domcontentloaded' | 'networkidle',
|
||||||
|
|
@ -2172,6 +2182,25 @@ export type FrameWaitForSelectorOptions = {
|
||||||
export type FrameWaitForSelectorResult = {
|
export type FrameWaitForSelectorResult = {
|
||||||
element?: ElementHandleChannel,
|
element?: ElementHandleChannel,
|
||||||
};
|
};
|
||||||
|
export type FrameExpectParams = {
|
||||||
|
selector: string,
|
||||||
|
expression: string,
|
||||||
|
expected?: ExpectedTextValue,
|
||||||
|
isNot?: boolean,
|
||||||
|
data?: any,
|
||||||
|
timeout?: number,
|
||||||
|
};
|
||||||
|
export type FrameExpectOptions = {
|
||||||
|
expected?: ExpectedTextValue,
|
||||||
|
isNot?: boolean,
|
||||||
|
data?: any,
|
||||||
|
timeout?: number,
|
||||||
|
};
|
||||||
|
export type FrameExpectResult = {
|
||||||
|
pass: boolean,
|
||||||
|
received: string,
|
||||||
|
log: string[],
|
||||||
|
};
|
||||||
|
|
||||||
export interface FrameEvents {
|
export interface FrameEvents {
|
||||||
'loadstate': FrameLoadstateEvent;
|
'loadstate': FrameLoadstateEvent;
|
||||||
|
|
@ -3734,6 +3763,7 @@ export const commandsWithTracingSnapshots = new Set([
|
||||||
'Frame.waitForTimeout',
|
'Frame.waitForTimeout',
|
||||||
'Frame.waitForFunction',
|
'Frame.waitForFunction',
|
||||||
'Frame.waitForSelector',
|
'Frame.waitForSelector',
|
||||||
|
'Frame.expect',
|
||||||
'JSHandle.evaluateExpression',
|
'JSHandle.evaluateExpression',
|
||||||
'ElementHandle.evaluateExpression',
|
'ElementHandle.evaluateExpression',
|
||||||
'JSHandle.evaluateExpressionHandle',
|
'JSHandle.evaluateExpressionHandle',
|
||||||
|
|
|
||||||
|
|
@ -97,6 +97,17 @@ SerializedArgument:
|
||||||
items: Channel
|
items: Channel
|
||||||
|
|
||||||
|
|
||||||
|
ExpectedTextValue:
|
||||||
|
type: object
|
||||||
|
properties:
|
||||||
|
string: string?
|
||||||
|
regexSource: string?
|
||||||
|
regexFlags: string?
|
||||||
|
matchSubstring: boolean?
|
||||||
|
normalizeWhiteSpace: boolean?
|
||||||
|
useInnerText: boolean?
|
||||||
|
|
||||||
|
|
||||||
AXNode:
|
AXNode:
|
||||||
type: object
|
type: object
|
||||||
properties:
|
properties:
|
||||||
|
|
@ -1742,6 +1753,21 @@ Frame:
|
||||||
tracing:
|
tracing:
|
||||||
snapshot: true
|
snapshot: true
|
||||||
|
|
||||||
|
expect:
|
||||||
|
parameters:
|
||||||
|
selector: string
|
||||||
|
expression: string
|
||||||
|
expected: ExpectedTextValue?
|
||||||
|
isNot: boolean?
|
||||||
|
data: json?
|
||||||
|
timeout: number?
|
||||||
|
returns:
|
||||||
|
pass: boolean
|
||||||
|
received: string
|
||||||
|
log: string[]
|
||||||
|
tracing:
|
||||||
|
snapshot: true
|
||||||
|
|
||||||
events:
|
events:
|
||||||
|
|
||||||
loadstate:
|
loadstate:
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,14 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
||||||
value: tType('SerializedValue'),
|
value: tType('SerializedValue'),
|
||||||
handles: tArray(tChannel('*')),
|
handles: tArray(tChannel('*')),
|
||||||
});
|
});
|
||||||
|
scheme.ExpectedTextValue = tObject({
|
||||||
|
string: tOptional(tString),
|
||||||
|
regexSource: tOptional(tString),
|
||||||
|
regexFlags: tOptional(tString),
|
||||||
|
matchSubstring: tOptional(tBoolean),
|
||||||
|
normalizeWhiteSpace: tOptional(tBoolean),
|
||||||
|
useInnerText: tOptional(tBoolean),
|
||||||
|
});
|
||||||
scheme.AXNode = tObject({
|
scheme.AXNode = tObject({
|
||||||
role: tString,
|
role: tString,
|
||||||
name: tString,
|
name: tString,
|
||||||
|
|
@ -876,6 +884,14 @@ export function createScheme(tChannel: (name: string) => Validator): Scheme {
|
||||||
timeout: tOptional(tNumber),
|
timeout: tOptional(tNumber),
|
||||||
state: tOptional(tEnum(['attached', 'detached', 'visible', 'hidden'])),
|
state: tOptional(tEnum(['attached', 'detached', 'visible', 'hidden'])),
|
||||||
});
|
});
|
||||||
|
scheme.FrameExpectParams = tObject({
|
||||||
|
selector: tString,
|
||||||
|
expression: tString,
|
||||||
|
expected: tOptional(tType('ExpectedTextValue')),
|
||||||
|
isNot: tOptional(tBoolean),
|
||||||
|
data: tOptional(tAny),
|
||||||
|
timeout: tOptional(tNumber),
|
||||||
|
});
|
||||||
scheme.WorkerEvaluateExpressionParams = tObject({
|
scheme.WorkerEvaluateExpressionParams = tObject({
|
||||||
expression: tString,
|
expression: tString,
|
||||||
isFunction: tOptional(tBoolean),
|
isFunction: tOptional(tBoolean),
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ import * as channels from '../protocol/channels';
|
||||||
import { FatalDOMError, RetargetableDOMError } from './common/domErrors';
|
import { FatalDOMError, RetargetableDOMError } from './common/domErrors';
|
||||||
import { isSessionClosedError } from './common/protocolError';
|
import { isSessionClosedError } from './common/protocolError';
|
||||||
import * as frames from './frames';
|
import * as frames from './frames';
|
||||||
import type { InjectedScript, InjectedScriptPoll } from './injected/injectedScript';
|
import type { InjectedScript, InjectedScriptPoll, LogEntry } from './injected/injectedScript';
|
||||||
import { CallMetadata } from './instrumentation';
|
import { CallMetadata } from './instrumentation';
|
||||||
import * as js from './javascript';
|
import * as js from './javascript';
|
||||||
import { Page } from './page';
|
import { Page } from './page';
|
||||||
|
|
@ -672,7 +672,7 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
|
|
||||||
async _setChecked(progress: Progress, state: boolean, options: { position?: types.Point } & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> {
|
async _setChecked(progress: Progress, state: boolean, options: { position?: types.Point } & types.PointerActionWaitOptions & types.NavigatingActionWaitOptions): Promise<'error:notconnected' | 'done'> {
|
||||||
const isChecked = async () => {
|
const isChecked = async () => {
|
||||||
const result = await this.evaluateInUtility(([injected, node]) => injected.checkElementState(node, 'checked'), {});
|
const result = await this.evaluateInUtility(([injected, node]) => injected.elementState(node, 'checked'), {});
|
||||||
return throwRetargetableDOMError(throwFatalDOMError(result));
|
return throwRetargetableDOMError(throwFatalDOMError(result));
|
||||||
};
|
};
|
||||||
if (await isChecked() === state)
|
if (await isChecked() === state)
|
||||||
|
|
@ -723,34 +723,34 @@ export class ElementHandle<T extends Node = Node> extends js.JSHandle<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
async isVisible(): Promise<boolean> {
|
async isVisible(): Promise<boolean> {
|
||||||
const result = await this.evaluateInUtility(([injected, node]) => injected.checkElementState(node, 'visible'), {});
|
const result = await this.evaluateInUtility(([injected, node]) => injected.elementState(node, 'visible'), {});
|
||||||
if (result === 'error:notconnected')
|
if (result === 'error:notconnected')
|
||||||
return false;
|
return false;
|
||||||
return throwFatalDOMError(result);
|
return throwFatalDOMError(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
async isHidden(): Promise<boolean> {
|
async isHidden(): Promise<boolean> {
|
||||||
const result = await this.evaluateInUtility(([injected, node]) => injected.checkElementState(node, 'hidden'), {});
|
const result = await this.evaluateInUtility(([injected, node]) => injected.elementState(node, 'hidden'), {});
|
||||||
return throwRetargetableDOMError(throwFatalDOMError(result));
|
return throwRetargetableDOMError(throwFatalDOMError(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
async isEnabled(): Promise<boolean> {
|
async isEnabled(): Promise<boolean> {
|
||||||
const result = await this.evaluateInUtility(([injected, node]) => injected.checkElementState(node, 'enabled'), {});
|
const result = await this.evaluateInUtility(([injected, node]) => injected.elementState(node, 'enabled'), {});
|
||||||
return throwRetargetableDOMError(throwFatalDOMError(result));
|
return throwRetargetableDOMError(throwFatalDOMError(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
async isDisabled(): Promise<boolean> {
|
async isDisabled(): Promise<boolean> {
|
||||||
const result = await this.evaluateInUtility(([injected, node]) => injected.checkElementState(node, 'disabled'), {});
|
const result = await this.evaluateInUtility(([injected, node]) => injected.elementState(node, 'disabled'), {});
|
||||||
return throwRetargetableDOMError(throwFatalDOMError(result));
|
return throwRetargetableDOMError(throwFatalDOMError(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
async isEditable(): Promise<boolean> {
|
async isEditable(): Promise<boolean> {
|
||||||
const result = await this.evaluateInUtility(([injected, node]) => injected.checkElementState(node, 'editable'), {});
|
const result = await this.evaluateInUtility(([injected, node]) => injected.elementState(node, 'editable'), {});
|
||||||
return throwRetargetableDOMError(throwFatalDOMError(result));
|
return throwRetargetableDOMError(throwFatalDOMError(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
async isChecked(): Promise<boolean> {
|
async isChecked(): Promise<boolean> {
|
||||||
const result = await this.evaluateInUtility(([injected, node]) => injected.checkElementState(node, 'checked'), {});
|
const result = await this.evaluateInUtility(([injected, node]) => injected.elementState(node, 'checked'), {});
|
||||||
return throwRetargetableDOMError(throwFatalDOMError(result));
|
return throwRetargetableDOMError(throwFatalDOMError(result));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -850,11 +850,11 @@ export class InjectedScriptPollHandler<T> {
|
||||||
|
|
||||||
private async _streamLogs() {
|
private async _streamLogs() {
|
||||||
while (this._poll && this._progress.isRunning()) {
|
while (this._poll && this._progress.isRunning()) {
|
||||||
const messages = await this._poll.evaluate(poll => poll.takeNextLogs()).catch(e => [] as string[]);
|
const log = await this._poll.evaluate(poll => poll.takeNextLogs()).catch(e => [] as LogEntry[]);
|
||||||
if (!this._poll || !this._progress.isRunning())
|
if (!this._poll || !this._progress.isRunning())
|
||||||
return;
|
return;
|
||||||
for (const message of messages)
|
for (const entry of log)
|
||||||
this._progress.log(message);
|
this._progress.logEntry(entry);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -886,9 +886,9 @@ export class InjectedScriptPollHandler<T> {
|
||||||
if (!this._poll)
|
if (!this._poll)
|
||||||
return;
|
return;
|
||||||
// Retrieve all the logs before continuing.
|
// Retrieve all the logs before continuing.
|
||||||
const messages = await this._poll.evaluate(poll => poll.takeLastLogs()).catch(e => [] as string[]);
|
const log = await this._poll.evaluate(poll => poll.takeLastLogs()).catch(e => [] as LogEntry[]);
|
||||||
for (const message of messages)
|
for (const entry of log)
|
||||||
this._progress.log(message);
|
this._progress.logEntry(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
async cancel() {
|
async cancel() {
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ export type NavigationEvent = {
|
||||||
};
|
};
|
||||||
|
|
||||||
export type SchedulableTask<T> = (injectedScript: js.JSHandle<InjectedScript>) => Promise<js.JSHandle<InjectedScriptPoll<T>>>;
|
export type SchedulableTask<T> = (injectedScript: js.JSHandle<InjectedScript>) => Promise<js.JSHandle<InjectedScriptPoll<T>>>;
|
||||||
export type DomTaskBody<T, R> = (progress: InjectedScriptProgress, element: Element, data: T) => R;
|
export type DomTaskBody<T, R> = (progress: InjectedScriptProgress, element: Element, data: T, continuePolling: any) => R;
|
||||||
|
|
||||||
export class FrameManager {
|
export class FrameManager {
|
||||||
private _page: Page;
|
private _page: Page;
|
||||||
|
|
@ -1067,10 +1067,10 @@ export class Frame extends SdkObject {
|
||||||
}, undefined, options);
|
}, undefined, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _checkElementState(metadata: CallMetadata, selector: string, state: ElementStateWithoutStable, options: types.QueryOnSelectorOptions = {}): Promise<boolean> {
|
private async _elementState(metadata: CallMetadata, selector: string, state: ElementStateWithoutStable, options: types.QueryOnSelectorOptions = {}): Promise<boolean> {
|
||||||
const result = await this._scheduleRerunnableTask(metadata, selector, (progress, element, data) => {
|
const result = await this._scheduleRerunnableTask(metadata, selector, (progress, element, data) => {
|
||||||
const injected = progress.injectedScript;
|
const injected = progress.injectedScript;
|
||||||
return injected.checkElementState(element, data.state);
|
return injected.elementState(element, data.state);
|
||||||
}, { state }, options);
|
}, { state }, options);
|
||||||
return dom.throwFatalDOMError(dom.throwRetargetableDOMError(result));
|
return dom.throwFatalDOMError(dom.throwRetargetableDOMError(result));
|
||||||
}
|
}
|
||||||
|
|
@ -1089,19 +1089,19 @@ export class Frame extends SdkObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
async isDisabled(metadata: CallMetadata, selector: string, options: types.QueryOnSelectorOptions = {}): Promise<boolean> {
|
async isDisabled(metadata: CallMetadata, selector: string, options: types.QueryOnSelectorOptions = {}): Promise<boolean> {
|
||||||
return this._checkElementState(metadata, selector, 'disabled', options);
|
return this._elementState(metadata, selector, 'disabled', options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async isEnabled(metadata: CallMetadata, selector: string, options: types.QueryOnSelectorOptions = {}): Promise<boolean> {
|
async isEnabled(metadata: CallMetadata, selector: string, options: types.QueryOnSelectorOptions = {}): Promise<boolean> {
|
||||||
return this._checkElementState(metadata, selector, 'enabled', options);
|
return this._elementState(metadata, selector, 'enabled', options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async isEditable(metadata: CallMetadata, selector: string, options: types.QueryOnSelectorOptions = {}): Promise<boolean> {
|
async isEditable(metadata: CallMetadata, selector: string, options: types.QueryOnSelectorOptions = {}): Promise<boolean> {
|
||||||
return this._checkElementState(metadata, selector, 'editable', options);
|
return this._elementState(metadata, selector, 'editable', options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async isChecked(metadata: CallMetadata, selector: string, options: types.QueryOnSelectorOptions = {}): Promise<boolean> {
|
async isChecked(metadata: CallMetadata, selector: string, options: types.QueryOnSelectorOptions = {}): Promise<boolean> {
|
||||||
return this._checkElementState(metadata, selector, 'checked', options);
|
return this._elementState(metadata, selector, 'checked', options);
|
||||||
}
|
}
|
||||||
|
|
||||||
async hover(metadata: CallMetadata, selector: string, options: types.PointerActionOptions & types.PointerActionWaitOptions = {}) {
|
async hover(metadata: CallMetadata, selector: string, options: types.PointerActionOptions & types.PointerActionWaitOptions = {}) {
|
||||||
|
|
@ -1160,6 +1160,81 @@ export class Frame extends SdkObject {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async expect(metadata: CallMetadata, selector: string, expression: string, options: channels.FrameExpectParams): Promise<{ pass: boolean, received: string, log: string[] }> {
|
||||||
|
const controller = new ProgressController(metadata, this);
|
||||||
|
return await this._scheduleRerunnableTaskWithController(controller, selector, (progress, element, data, continuePolling) => {
|
||||||
|
const injected = progress.injectedScript;
|
||||||
|
const matcher = data.expected ? injected.expectedTextMatcher(data.expected) : null;
|
||||||
|
let received: string;
|
||||||
|
let elementState: boolean | 'error:notconnected' | 'error:notcheckbox' | undefined;
|
||||||
|
|
||||||
|
if (data.expression === 'to.be.checked') {
|
||||||
|
elementState = progress.injectedScript.elementState(element, 'checked');
|
||||||
|
} else if (data.expression === 'to.be.disabled') {
|
||||||
|
elementState = progress.injectedScript.elementState(element, 'disabled');
|
||||||
|
} else if (data.expression === 'to.be.editable') {
|
||||||
|
elementState = progress.injectedScript.elementState(element, 'editable');
|
||||||
|
} else if (data.expression === 'to.be.empty') {
|
||||||
|
if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA')
|
||||||
|
elementState = !(element as HTMLInputElement).value;
|
||||||
|
else
|
||||||
|
elementState = !element.textContent?.trim();
|
||||||
|
} else if (data.expression === 'to.be.enabled') {
|
||||||
|
elementState = progress.injectedScript.elementState(element, 'enabled');
|
||||||
|
} else if (data.expression === 'to.be.focused') {
|
||||||
|
elementState = document.activeElement === element;
|
||||||
|
} else if (data.expression === 'to.be.hidden') {
|
||||||
|
elementState = progress.injectedScript.elementState(element, 'hidden');
|
||||||
|
} else if (data.expression === 'to.be.visible') {
|
||||||
|
elementState = progress.injectedScript.elementState(element, 'visible');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elementState !== undefined) {
|
||||||
|
if (elementState === 'error:notcheckbox')
|
||||||
|
throw injected.createStacklessError('Element is not a checkbox');
|
||||||
|
if (elementState === 'error:notconnected')
|
||||||
|
throw injected.createStacklessError('Element is not connected');
|
||||||
|
if (elementState === data.isNot)
|
||||||
|
return continuePolling;
|
||||||
|
return { pass: !data.isNot };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.expression === 'to.have.attribute') {
|
||||||
|
received = element.getAttribute(data.data.name) || '';
|
||||||
|
} else if (data.expression === 'to.have.class') {
|
||||||
|
received = element.className;
|
||||||
|
} else if (data.expression === 'to.have.css') {
|
||||||
|
received = (window.getComputedStyle(element) as any)[data.data.name];
|
||||||
|
} else if (data.expression === 'to.have.id') {
|
||||||
|
received = element.id;
|
||||||
|
} else if (data.expression === 'to.have.text') {
|
||||||
|
received = data.expected!.useInnerText ? (element as HTMLElement).innerText : element.textContent || '';
|
||||||
|
} else if (data.expression === 'to.have.title') {
|
||||||
|
received = document.title;
|
||||||
|
} else if (data.expression === 'to.have.url') {
|
||||||
|
received = document.location.href;
|
||||||
|
} else if (data.expression === 'to.have.value') {
|
||||||
|
if (element.nodeName !== 'INPUT' && element.nodeName !== 'TEXTAREA' && element.nodeName !== 'SELECT') {
|
||||||
|
progress.log('Not an input element');
|
||||||
|
return 'error:hasnovalue';
|
||||||
|
}
|
||||||
|
received = (element as any).value;
|
||||||
|
} else {
|
||||||
|
throw new Error(`Internal error, unknown matcher ${data.expression}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
progress.setIntermediateResult(received);
|
||||||
|
|
||||||
|
if (matcher && matcher.matches(received) === data.isNot)
|
||||||
|
return continuePolling;
|
||||||
|
return { received, pass: !data.isNot } as any;
|
||||||
|
}, { expression, expected: options.expected, isNot: options.isNot, data: options.data }, { strict: true, ...options }).catch(e => {
|
||||||
|
if (js.isJavaScriptErrorInEvaluate(e))
|
||||||
|
throw e;
|
||||||
|
return { received: controller.lastIntermediateResult(), pass: options.isNot, log: metadata.log };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async _waitForFunctionExpression<R>(metadata: CallMetadata, expression: string, isFunction: boolean | undefined, arg: any, options: types.WaitForFunctionOptions, world: types.World = 'main'): Promise<js.SmartHandle<R>> {
|
async _waitForFunctionExpression<R>(metadata: CallMetadata, expression: string, isFunction: boolean | undefined, arg: any, options: types.WaitForFunctionOptions, world: types.World = 'main'): Promise<js.SmartHandle<R>> {
|
||||||
const controller = new ProgressController(metadata, this);
|
const controller = new ProgressController(metadata, this);
|
||||||
if (typeof options.pollingInterval === 'number')
|
if (typeof options.pollingInterval === 'number')
|
||||||
|
|
@ -1219,6 +1294,10 @@ export class Frame extends SdkObject {
|
||||||
|
|
||||||
private async _scheduleRerunnableTask<T, R>(metadata: CallMetadata, selector: string, body: DomTaskBody<T, R>, taskData: T, options: types.TimeoutOptions & types.StrictOptions & { mainWorld?: boolean } = {}): Promise<R> {
|
private async _scheduleRerunnableTask<T, R>(metadata: CallMetadata, selector: string, body: DomTaskBody<T, R>, taskData: T, options: types.TimeoutOptions & types.StrictOptions & { mainWorld?: boolean } = {}): Promise<R> {
|
||||||
const controller = new ProgressController(metadata, this);
|
const controller = new ProgressController(metadata, this);
|
||||||
|
return this._scheduleRerunnableTaskWithController(controller, selector, body, taskData, options);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _scheduleRerunnableTaskWithController<T, R>(controller: ProgressController, selector: string, body: DomTaskBody<T, R>, taskData: T, options: types.TimeoutOptions & types.StrictOptions & { mainWorld?: boolean } = {}): Promise<R> {
|
||||||
const info = this._page.parseSelector(selector, options);
|
const info = this._page.parseSelector(selector, options);
|
||||||
const callbackText = body.toString();
|
const callbackText = body.toString();
|
||||||
const data = this._contextData.get(options.mainWorld ? 'main' : info.world)!;
|
const data = this._contextData.get(options.mainWorld ? 'main' : info.world)!;
|
||||||
|
|
@ -1231,8 +1310,8 @@ export class Frame extends SdkObject {
|
||||||
const element = injected.querySelector(info.parsed, document, info.strict);
|
const element = injected.querySelector(info.parsed, document, info.strict);
|
||||||
if (!element)
|
if (!element)
|
||||||
return continuePolling;
|
return continuePolling;
|
||||||
progress.log(` selector resolved to ${injected.previewNode(element)}`);
|
progress.logRepeating(` selector resolved to ${injected.previewNode(element)}`);
|
||||||
return callback(progress, element, taskData);
|
return callback(progress, element, taskData, continuePolling);
|
||||||
});
|
});
|
||||||
}, { info, taskData, callbackText });
|
}, { info, taskData, callbackText });
|
||||||
}, true);
|
}, true);
|
||||||
|
|
|
||||||
|
|
@ -23,22 +23,29 @@ import { FatalDOMError } from '../common/domErrors';
|
||||||
import { SelectorEvaluatorImpl, isVisible, parentElementOrShadowHost, elementMatchesText, TextMatcher, createRegexTextMatcher, createStrictTextMatcher, createLaxTextMatcher } from './selectorEvaluator';
|
import { SelectorEvaluatorImpl, isVisible, parentElementOrShadowHost, elementMatchesText, TextMatcher, createRegexTextMatcher, createStrictTextMatcher, createLaxTextMatcher } from './selectorEvaluator';
|
||||||
import { CSSComplexSelectorList } from '../common/cssParser';
|
import { CSSComplexSelectorList } from '../common/cssParser';
|
||||||
import { generateSelector } from './selectorGenerator';
|
import { generateSelector } from './selectorGenerator';
|
||||||
|
import type { ExpectedTextValue } from '../../protocol/channels';
|
||||||
|
|
||||||
type Predicate<T> = (progress: InjectedScriptProgress, continuePolling: symbol) => T | symbol;
|
type Predicate<T> = (progress: InjectedScriptProgress, continuePolling: symbol) => T | symbol;
|
||||||
|
|
||||||
export type InjectedScriptProgress = {
|
export type InjectedScriptProgress = {
|
||||||
injectedScript: InjectedScript,
|
injectedScript: InjectedScript;
|
||||||
aborted: boolean,
|
aborted: boolean;
|
||||||
log: (message: string) => void,
|
log: (message: string) => void;
|
||||||
logRepeating: (message: string) => void,
|
logRepeating: (message: string) => void;
|
||||||
|
setIntermediateResult: (intermediateResult: any) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type LogEntry = {
|
||||||
|
message?: string;
|
||||||
|
intermediateResult?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type InjectedScriptPoll<T> = {
|
export type InjectedScriptPoll<T> = {
|
||||||
run: () => Promise<T>,
|
run: () => Promise<T>,
|
||||||
// Takes more logs, waiting until at least one message is available.
|
// Takes more logs, waiting until at least one message is available.
|
||||||
takeNextLogs: () => Promise<string[]>,
|
takeNextLogs: () => Promise<LogEntry[]>,
|
||||||
// Takes all current logs without waiting.
|
// Takes all current logs without waiting.
|
||||||
takeLastLogs: () => string[],
|
takeLastLogs: () => LogEntry[],
|
||||||
cancel: () => void,
|
cancel: () => void,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -311,36 +318,44 @@ export class InjectedScript {
|
||||||
}
|
}
|
||||||
|
|
||||||
private _runAbortableTask<T>(task: (progess: InjectedScriptProgress) => Promise<T>): InjectedScriptPoll<T> {
|
private _runAbortableTask<T>(task: (progess: InjectedScriptProgress) => Promise<T>): InjectedScriptPoll<T> {
|
||||||
let unsentLogs: string[] = [];
|
let unsentLog: LogEntry[] = [];
|
||||||
let takeNextLogsCallback: ((logs: string[]) => void) | undefined;
|
let takeNextLogsCallback: ((logs: LogEntry[]) => void) | undefined;
|
||||||
let taskFinished = false;
|
let taskFinished = false;
|
||||||
const logReady = () => {
|
const logReady = () => {
|
||||||
if (!takeNextLogsCallback)
|
if (!takeNextLogsCallback)
|
||||||
return;
|
return;
|
||||||
takeNextLogsCallback(unsentLogs);
|
takeNextLogsCallback(unsentLog);
|
||||||
unsentLogs = [];
|
unsentLog = [];
|
||||||
takeNextLogsCallback = undefined;
|
takeNextLogsCallback = undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const takeNextLogs = () => new Promise<string[]>(fulfill => {
|
const takeNextLogs = () => new Promise<LogEntry[]>(fulfill => {
|
||||||
takeNextLogsCallback = fulfill;
|
takeNextLogsCallback = fulfill;
|
||||||
if (unsentLogs.length || taskFinished)
|
if (unsentLog.length || taskFinished)
|
||||||
logReady();
|
logReady();
|
||||||
});
|
});
|
||||||
|
|
||||||
let lastLog = '';
|
let lastMessage = '';
|
||||||
|
let lastIntermediateResult: any = undefined;
|
||||||
const progress: InjectedScriptProgress = {
|
const progress: InjectedScriptProgress = {
|
||||||
injectedScript: this,
|
injectedScript: this,
|
||||||
aborted: false,
|
aborted: false,
|
||||||
log: (message: string) => {
|
log: (message: string) => {
|
||||||
lastLog = message;
|
lastMessage = message;
|
||||||
unsentLogs.push(message);
|
unsentLog.push({ message });
|
||||||
logReady();
|
logReady();
|
||||||
},
|
},
|
||||||
logRepeating: (message: string) => {
|
logRepeating: (message: string) => {
|
||||||
if (message !== lastLog)
|
if (message !== lastMessage)
|
||||||
progress.log(message);
|
progress.log(message);
|
||||||
},
|
},
|
||||||
|
setIntermediateResult: (intermediateResult: any) => {
|
||||||
|
if (lastIntermediateResult === intermediateResult)
|
||||||
|
return;
|
||||||
|
lastIntermediateResult = intermediateResult;
|
||||||
|
unsentLog.push({ intermediateResult });
|
||||||
|
logReady();
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const run = () => {
|
const run = () => {
|
||||||
|
|
@ -361,7 +376,7 @@ export class InjectedScript {
|
||||||
takeNextLogs,
|
takeNextLogs,
|
||||||
run,
|
run,
|
||||||
cancel: () => { progress.aborted = true; },
|
cancel: () => { progress.aborted = true; },
|
||||||
takeLastLogs: () => unsentLogs,
|
takeLastLogs: () => unsentLog,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -405,7 +420,7 @@ export class InjectedScript {
|
||||||
|
|
||||||
for (const state of states) {
|
for (const state of states) {
|
||||||
if (state !== 'stable') {
|
if (state !== 'stable') {
|
||||||
const result = this.checkElementState(node, state);
|
const result = this.elementState(node, state);
|
||||||
if (typeof result !== 'boolean')
|
if (typeof result !== 'boolean')
|
||||||
return result;
|
return result;
|
||||||
if (!result) {
|
if (!result) {
|
||||||
|
|
@ -456,7 +471,7 @@ export class InjectedScript {
|
||||||
return this.pollRaf(predicate);
|
return this.pollRaf(predicate);
|
||||||
}
|
}
|
||||||
|
|
||||||
checkElementState(node: Node, state: ElementStateWithoutStable): boolean | 'error:notconnected' | FatalDOMError {
|
elementState(node: Node, state: ElementStateWithoutStable): boolean | 'error:notconnected' | 'error:notcheckbox' {
|
||||||
const element = this.retarget(node, ['stable', 'visible', 'hidden'].includes(state) ? 'no-follow-label' : 'follow-label');
|
const element = this.retarget(node, ['stable', 'visible', 'hidden'].includes(state) ? 'no-follow-label' : 'follow-label');
|
||||||
if (!element || !element.isConnected) {
|
if (!element || !element.isConnected) {
|
||||||
if (state === 'hidden')
|
if (state === 'hidden')
|
||||||
|
|
@ -761,6 +776,10 @@ export class InjectedScript {
|
||||||
delete error.stack;
|
delete error.stack;
|
||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
expectedTextMatcher(expected: ExpectedTextValue): ExpectedTextMatcher {
|
||||||
|
return new ExpectedTextMatcher(expected);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const autoClosingTags = new Set(['AREA', 'BASE', 'BR', 'COL', 'COMMAND', 'EMBED', 'HR', 'IMG', 'INPUT', 'KEYGEN', 'LINK', 'MENUITEM', 'META', 'PARAM', 'SOURCE', 'TRACK', 'WBR']);
|
const autoClosingTags = new Set(['AREA', 'BASE', 'BR', 'COL', 'COMMAND', 'EMBED', 'HR', 'IMG', 'INPUT', 'KEYGEN', 'LINK', 'MENUITEM', 'META', 'PARAM', 'SOURCE', 'TRACK', 'WBR']);
|
||||||
|
|
@ -850,4 +869,36 @@ function createTextMatcher(selector: string): { matcher: TextMatcher, kind: 'reg
|
||||||
return { matcher, kind: strict ? 'strict' : 'lax' };
|
return { matcher, kind: strict ? 'strict' : 'lax' };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
class ExpectedTextMatcher {
|
||||||
|
_string: string | undefined;
|
||||||
|
private _substring: string | undefined;
|
||||||
|
private _regex: RegExp | undefined;
|
||||||
|
private _normalizeWhiteSpace: boolean | undefined;
|
||||||
|
|
||||||
|
constructor(expected: ExpectedTextValue) {
|
||||||
|
this._normalizeWhiteSpace = expected.normalizeWhiteSpace;
|
||||||
|
this._string = expected.matchSubstring ? undefined : this.normalizeWhiteSpace(expected.string);
|
||||||
|
this._substring = expected.matchSubstring ? this.normalizeWhiteSpace(expected.string) : undefined;
|
||||||
|
this._regex = expected.regexSource ? new RegExp(expected.regexSource, expected.regexFlags) : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
matches(text: string): boolean {
|
||||||
|
if (this._normalizeWhiteSpace && !this._regex)
|
||||||
|
text = this.normalizeWhiteSpace(text)!;
|
||||||
|
if (this._string !== undefined)
|
||||||
|
return text === this._string;
|
||||||
|
if (this._substring !== undefined)
|
||||||
|
return text.includes(this._substring);
|
||||||
|
if (this._regex)
|
||||||
|
return !!this._regex.test(text);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private normalizeWhiteSpace(s: string | undefined): string | undefined {
|
||||||
|
if (!s)
|
||||||
|
return s;
|
||||||
|
return this._normalizeWhiteSpace ? s.trim().replace(/\s+/g, ' ') : s;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default InjectedScript;
|
export default InjectedScript;
|
||||||
|
|
|
||||||
|
|
@ -19,9 +19,12 @@ import { assert, monotonicTime } from '../utils/utils';
|
||||||
import { LogName } from '../utils/debugLogger';
|
import { LogName } from '../utils/debugLogger';
|
||||||
import { CallMetadata, Instrumentation, SdkObject } from './instrumentation';
|
import { CallMetadata, Instrumentation, SdkObject } from './instrumentation';
|
||||||
import { ElementHandle } from './dom';
|
import { ElementHandle } from './dom';
|
||||||
|
import { ManualPromise } from '../utils/async';
|
||||||
|
import type { LogEntry } from './injected/injectedScript';
|
||||||
|
|
||||||
export interface Progress {
|
export interface Progress {
|
||||||
log(message: string): void;
|
log(message: string): void;
|
||||||
|
logEntry(entry: LogEntry): void;
|
||||||
timeUntilDeadline(): number;
|
timeUntilDeadline(): number;
|
||||||
isRunning(): boolean;
|
isRunning(): boolean;
|
||||||
cleanupWhenAborted(cleanup: () => any): void;
|
cleanupWhenAborted(cleanup: () => any): void;
|
||||||
|
|
@ -31,10 +34,7 @@ export interface Progress {
|
||||||
}
|
}
|
||||||
|
|
||||||
export class ProgressController {
|
export class ProgressController {
|
||||||
// Promise and callback that forcefully abort the progress.
|
private _forceAbortPromise = new ManualPromise<any>();
|
||||||
// This promise always rejects.
|
|
||||||
private _forceAbort: (error: Error) => void = () => {};
|
|
||||||
private _forceAbortPromise: Promise<any>;
|
|
||||||
|
|
||||||
// Cleanups to be run only in the case of abort.
|
// Cleanups to be run only in the case of abort.
|
||||||
private _cleanups: (() => any)[] = [];
|
private _cleanups: (() => any)[] = [];
|
||||||
|
|
@ -43,6 +43,7 @@ export class ProgressController {
|
||||||
private _state: 'before' | 'running' | 'aborted' | 'finished' = 'before';
|
private _state: 'before' | 'running' | 'aborted' | 'finished' = 'before';
|
||||||
private _deadline: number = 0;
|
private _deadline: number = 0;
|
||||||
private _timeout: number = 0;
|
private _timeout: number = 0;
|
||||||
|
private _lastIntermediateResult: any;
|
||||||
readonly metadata: CallMetadata;
|
readonly metadata: CallMetadata;
|
||||||
readonly instrumentation: Instrumentation;
|
readonly instrumentation: Instrumentation;
|
||||||
readonly sdkObject: SdkObject;
|
readonly sdkObject: SdkObject;
|
||||||
|
|
@ -51,7 +52,6 @@ export class ProgressController {
|
||||||
this.metadata = metadata;
|
this.metadata = metadata;
|
||||||
this.sdkObject = sdkObject;
|
this.sdkObject = sdkObject;
|
||||||
this.instrumentation = sdkObject.instrumentation;
|
this.instrumentation = sdkObject.instrumentation;
|
||||||
this._forceAbortPromise = new Promise((resolve, reject) => this._forceAbort = reject);
|
|
||||||
this._forceAbortPromise.catch(e => null); // Prevent unhandled promise rejection.
|
this._forceAbortPromise.catch(e => null); // Prevent unhandled promise rejection.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -59,6 +59,10 @@ export class ProgressController {
|
||||||
this._logName = logName;
|
this._logName = logName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lastIntermediateResult() {
|
||||||
|
return this._lastIntermediateResult;
|
||||||
|
}
|
||||||
|
|
||||||
async run<T>(task: (progress: Progress) => Promise<T>, timeout?: number): Promise<T> {
|
async run<T>(task: (progress: Progress) => Promise<T>, timeout?: number): Promise<T> {
|
||||||
if (timeout) {
|
if (timeout) {
|
||||||
this._timeout = timeout;
|
this._timeout = timeout;
|
||||||
|
|
@ -70,10 +74,18 @@ export class ProgressController {
|
||||||
|
|
||||||
const progress: Progress = {
|
const progress: Progress = {
|
||||||
log: message => {
|
log: message => {
|
||||||
if (this._state === 'running')
|
progress.logEntry({ message });
|
||||||
this.metadata.log.push(message);
|
},
|
||||||
// Note: we might be sending logs after progress has finished, for example browser logs.
|
logEntry: entry => {
|
||||||
this.instrumentation.onCallLog(this._logName, message, this.sdkObject, this.metadata);
|
if ('message' in entry) {
|
||||||
|
const message = entry.message!;
|
||||||
|
if (this._state === 'running')
|
||||||
|
this.metadata.log.push(message);
|
||||||
|
// Note: we might be sending logs after progress has finished, for example browser logs.
|
||||||
|
this.instrumentation.onCallLog(this._logName, message, this.sdkObject, this.metadata);
|
||||||
|
}
|
||||||
|
if ('intermediateResult' in entry)
|
||||||
|
this._lastIntermediateResult = entry.intermediateResult;
|
||||||
},
|
},
|
||||||
timeUntilDeadline: () => this._deadline ? this._deadline - monotonicTime() : 2147483647, // 2^31-1 safe setTimeout in Node.
|
timeUntilDeadline: () => this._deadline ? this._deadline - monotonicTime() : 2147483647, // 2^31-1 safe setTimeout in Node.
|
||||||
isRunning: () => this._state === 'running',
|
isRunning: () => this._state === 'running',
|
||||||
|
|
@ -94,7 +106,7 @@ export class ProgressController {
|
||||||
};
|
};
|
||||||
|
|
||||||
const timeoutError = new TimeoutError(`Timeout ${this._timeout}ms exceeded.`);
|
const timeoutError = new TimeoutError(`Timeout ${this._timeout}ms exceeded.`);
|
||||||
const timer = setTimeout(() => this._forceAbort(timeoutError), progress.timeUntilDeadline());
|
const timer = setTimeout(() => this._forceAbortPromise.reject(timeoutError), progress.timeUntilDeadline());
|
||||||
try {
|
try {
|
||||||
const promise = task(progress);
|
const promise = task(progress);
|
||||||
const result = await Promise.race([promise, this._forceAbortPromise]);
|
const result = await Promise.race([promise, this._forceAbortPromise]);
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
|
|
||||||
import { installTransform } from './transform';
|
import { installTransform } from './transform';
|
||||||
import type { FullConfig, Config, FullProject, Project, ReporterDescription, PreserveOutput } from './types';
|
import type { FullConfig, Config, FullProject, Project, ReporterDescription, PreserveOutput } from './types';
|
||||||
import { isRegExp, mergeObjects, errorWithFile } from './util';
|
import { mergeObjects, errorWithFile } from './util';
|
||||||
import { setCurrentlyLoadingFileSuite } from './globals';
|
import { setCurrentlyLoadingFileSuite } from './globals';
|
||||||
import { Suite } from './test';
|
import { Suite } from './test';
|
||||||
import { SerializedLoaderData } from './ipc';
|
import { SerializedLoaderData } from './ipc';
|
||||||
|
|
@ -26,6 +26,7 @@ import * as fs from 'fs';
|
||||||
import { ProjectImpl } from './project';
|
import { ProjectImpl } from './project';
|
||||||
import { Reporter } from '../../types/testReporter';
|
import { Reporter } from '../../types/testReporter';
|
||||||
import { BuiltInReporter, builtInReporters } from './runner';
|
import { BuiltInReporter, builtInReporters } from './runner';
|
||||||
|
import { isRegExp } from '../utils/utils';
|
||||||
|
|
||||||
export class Loader {
|
export class Loader {
|
||||||
private _defaultConfig: Config;
|
private _defaultConfig: Config;
|
||||||
|
|
|
||||||
|
|
@ -26,8 +26,8 @@ export function toBeChecked(
|
||||||
locator: Locator,
|
locator: Locator,
|
||||||
options?: { timeout?: number },
|
options?: { timeout?: number },
|
||||||
) {
|
) {
|
||||||
return toBeTruthy.call(this, 'toBeChecked', locator, 'Locator', async timeout => {
|
return toBeTruthy.call(this, 'toBeChecked', locator, 'Locator', async (isNot, timeout) => {
|
||||||
return await locator.isChecked({ timeout });
|
return await (locator as any)._expect('to.be.checked', { isNot, timeout });
|
||||||
}, options);
|
}, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -36,8 +36,8 @@ export function toBeDisabled(
|
||||||
locator: Locator,
|
locator: Locator,
|
||||||
options?: { timeout?: number },
|
options?: { timeout?: number },
|
||||||
) {
|
) {
|
||||||
return toBeTruthy.call(this, 'toBeDisabled', locator, 'Locator', async timeout => {
|
return toBeTruthy.call(this, 'toBeDisabled', locator, 'Locator', async (isNot, timeout) => {
|
||||||
return await locator.isDisabled({ timeout });
|
return await (locator as any)._expect('to.be.disabled', { isNot, timeout });
|
||||||
}, options);
|
}, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -46,8 +46,8 @@ export function toBeEditable(
|
||||||
locator: Locator,
|
locator: Locator,
|
||||||
options?: { timeout?: number },
|
options?: { timeout?: number },
|
||||||
) {
|
) {
|
||||||
return toBeTruthy.call(this, 'toBeEditable', locator, 'Locator', async timeout => {
|
return toBeTruthy.call(this, 'toBeEditable', locator, 'Locator', async (isNot, timeout) => {
|
||||||
return await locator.isEditable({ timeout });
|
return await (locator as any)._expect('to.be.editable', { isNot, timeout });
|
||||||
}, options);
|
}, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -56,12 +56,8 @@ export function toBeEmpty(
|
||||||
locator: Locator,
|
locator: Locator,
|
||||||
options?: { timeout?: number },
|
options?: { timeout?: number },
|
||||||
) {
|
) {
|
||||||
return toBeTruthy.call(this, 'toBeEmpty', locator, 'Locator', async timeout => {
|
return toBeTruthy.call(this, 'toBeEmpty', locator, 'Locator', async (isNot, timeout) => {
|
||||||
return await locator.evaluate(element => {
|
return await (locator as any)._expect('to.be.empty', { isNot, timeout });
|
||||||
if (element.nodeName === 'INPUT' || element.nodeName === 'TEXTAREA')
|
|
||||||
return !(element as HTMLInputElement).value;
|
|
||||||
return !element.textContent?.trim();
|
|
||||||
}, { timeout });
|
|
||||||
}, options);
|
}, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -70,8 +66,8 @@ export function toBeEnabled(
|
||||||
locator: Locator,
|
locator: Locator,
|
||||||
options?: { timeout?: number },
|
options?: { timeout?: number },
|
||||||
) {
|
) {
|
||||||
return toBeTruthy.call(this, 'toBeEnabled', locator, 'Locator', async timeout => {
|
return toBeTruthy.call(this, 'toBeEnabled', locator, 'Locator', async (isNot, timeout) => {
|
||||||
return await locator.isEnabled({ timeout });
|
return await (locator as any)._expect('to.be.enabled', { isNot, timeout });
|
||||||
}, options);
|
}, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -80,10 +76,8 @@ export function toBeFocused(
|
||||||
locator: Locator,
|
locator: Locator,
|
||||||
options?: { timeout?: number },
|
options?: { timeout?: number },
|
||||||
) {
|
) {
|
||||||
return toBeTruthy.call(this, 'toBeFocused', locator, 'Locator', async timeout => {
|
return toBeTruthy.call(this, 'toBeFocused', locator, 'Locator', async (isNot, timeout) => {
|
||||||
return await locator.evaluate(element => {
|
return await (locator as any)._expect('to.be.focused', { isNot, timeout });
|
||||||
return document.activeElement === element;
|
|
||||||
}, { timeout });
|
|
||||||
}, options);
|
}, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -92,8 +86,8 @@ export function toBeHidden(
|
||||||
locator: Locator,
|
locator: Locator,
|
||||||
options?: { timeout?: number },
|
options?: { timeout?: number },
|
||||||
) {
|
) {
|
||||||
return toBeTruthy.call(this, 'toBeHidden', locator, 'Locator', async timeout => {
|
return toBeTruthy.call(this, 'toBeHidden', locator, 'Locator', async (isNot, timeout) => {
|
||||||
return await locator.isHidden({ timeout });
|
return await (locator as any)._expect('to.be.hidden', { isNot, timeout });
|
||||||
}, options);
|
}, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -102,8 +96,8 @@ export function toBeVisible(
|
||||||
locator: Locator,
|
locator: Locator,
|
||||||
options?: { timeout?: number },
|
options?: { timeout?: number },
|
||||||
) {
|
) {
|
||||||
return toBeTruthy.call(this, 'toBeVisible', locator, 'Locator', async timeout => {
|
return toBeTruthy.call(this, 'toBeVisible', locator, 'Locator', async (isNot, timeout) => {
|
||||||
return await locator.isVisible({ timeout });
|
return await (locator as any)._expect('to.be.visible', { isNot, timeout });
|
||||||
}, options);
|
}, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -113,11 +107,9 @@ export function toContainText(
|
||||||
expected: string,
|
expected: string,
|
||||||
options?: { timeout?: number, useInnerText?: boolean },
|
options?: { timeout?: number, useInnerText?: boolean },
|
||||||
) {
|
) {
|
||||||
return toMatchText.call(this, 'toContainText', locator, 'Locator', async timeout => {
|
return toMatchText.call(this, 'toContainText', locator, 'Locator', async (expected, isNot, timeout) => {
|
||||||
if (options?.useInnerText)
|
return await (locator as any)._expect('to.have.text', { expected, isNot, timeout });
|
||||||
return await locator.innerText({ timeout });
|
}, expected, { ...options, matchSubstring: true, normalizeWhiteSpace: true, useInnerText: options?.useInnerText });
|
||||||
return await locator.textContent() || '';
|
|
||||||
}, expected, { ...options, matchSubstring: true, normalizeWhiteSpace: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toHaveAttribute(
|
export function toHaveAttribute(
|
||||||
|
|
@ -127,8 +119,8 @@ export function toHaveAttribute(
|
||||||
expected: string | RegExp,
|
expected: string | RegExp,
|
||||||
options?: { timeout?: number },
|
options?: { timeout?: number },
|
||||||
) {
|
) {
|
||||||
return toMatchText.call(this, 'toHaveAttribute', locator, 'Locator', async timeout => {
|
return toMatchText.call(this, 'toHaveAttribute', locator, 'Locator', async (expected, isNot, timeout) => {
|
||||||
return await locator.getAttribute(name, { timeout }) || '';
|
return await (locator as any)._expect('to.have.attribute', { expected, isNot, timeout, data: { name } });
|
||||||
}, expected, options);
|
}, expected, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -143,8 +135,8 @@ export function toHaveClass(
|
||||||
return await locator.evaluateAll(ee => ee.map(e => e.className));
|
return await locator.evaluateAll(ee => ee.map(e => e.className));
|
||||||
}, expected, options);
|
}, expected, options);
|
||||||
} else {
|
} else {
|
||||||
return toMatchText.call(this, 'toHaveClass', locator, 'Locator', async timeout => {
|
return toMatchText.call(this, 'toHaveClass', locator, 'Locator', async (expected, isNot, timeout) => {
|
||||||
return await locator.evaluate(element => element.className, { timeout });
|
return await (locator as any)._expect('to.have.class', { expected, isNot, timeout });
|
||||||
}, expected, options);
|
}, expected, options);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -167,10 +159,8 @@ export function toHaveCSS(
|
||||||
expected: string | RegExp,
|
expected: string | RegExp,
|
||||||
options?: { timeout?: number },
|
options?: { timeout?: number },
|
||||||
) {
|
) {
|
||||||
return toMatchText.call(this, 'toHaveCSS', locator, 'Locator', async timeout => {
|
return toMatchText.call(this, 'toHaveCSS', locator, 'Locator', async (expected, isNot, timeout) => {
|
||||||
return await locator.evaluate(async (element, name) => {
|
return await (locator as any)._expect('to.have.css', { expected, isNot, timeout, data: { name } });
|
||||||
return (window.getComputedStyle(element) as any)[name];
|
|
||||||
}, name, { timeout });
|
|
||||||
}, expected, options);
|
}, expected, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -180,8 +170,8 @@ export function toHaveId(
|
||||||
expected: string | RegExp,
|
expected: string | RegExp,
|
||||||
options?: { timeout?: number },
|
options?: { timeout?: number },
|
||||||
) {
|
) {
|
||||||
return toMatchText.call(this, 'toHaveId', locator, 'Locator', async timeout => {
|
return toMatchText.call(this, 'toHaveId', locator, 'Locator', async (expected, isNot, timeout) => {
|
||||||
return await locator.getAttribute('id', { timeout }) || '';
|
return await (locator as any)._expect('to.have.id', { expected, isNot, timeout });
|
||||||
}, expected, options);
|
}, expected, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -213,11 +203,9 @@ export function toHaveText(
|
||||||
return texts.map((s, index) => isString(expectedArray[index]) ? normalizeWhiteSpace(s) : s);
|
return texts.map((s, index) => isString(expectedArray[index]) ? normalizeWhiteSpace(s) : s);
|
||||||
}, expectedArray, options);
|
}, expectedArray, options);
|
||||||
} else {
|
} else {
|
||||||
return toMatchText.call(this, 'toHaveText', locator, 'Locator', async timeout => {
|
return toMatchText.call(this, 'toHaveText', locator, 'Locator', async (expected, isNot, timeout) => {
|
||||||
if (options?.useInnerText)
|
return await (locator as any)._expect('to.have.text', { expected, isNot, timeout });
|
||||||
return await locator.innerText({ timeout });
|
}, expected, { ...options, normalizeWhiteSpace: true, useInnerText: options?.useInnerText });
|
||||||
return await locator.textContent() || '';
|
|
||||||
}, expected, { ...options, normalizeWhiteSpace: true });
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -227,8 +215,9 @@ export function toHaveTitle(
|
||||||
expected: string | RegExp,
|
expected: string | RegExp,
|
||||||
options: { timeout?: number } = {},
|
options: { timeout?: number } = {},
|
||||||
) {
|
) {
|
||||||
return toMatchText.call(this, 'toHaveTitle', page, 'Page', async () => {
|
const locator = page.locator(':root');
|
||||||
return await page.title();
|
return toMatchText.call(this, 'toHaveTitle', locator, 'Locator', async (expected, isNot, timeout) => {
|
||||||
|
return await (locator as any)._expect('to.have.title', { expected, isNot, timeout });
|
||||||
}, expected, { ...options, normalizeWhiteSpace: true });
|
}, expected, { ...options, normalizeWhiteSpace: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -239,10 +228,11 @@ export function toHaveURL(
|
||||||
options?: { timeout?: number },
|
options?: { timeout?: number },
|
||||||
) {
|
) {
|
||||||
const baseURL = (page.context() as any)._options.baseURL;
|
const baseURL = (page.context() as any)._options.baseURL;
|
||||||
|
expected = typeof expected === 'string' ? constructURLBasedOnBaseURL(baseURL, expected) : expected;
|
||||||
return toMatchText.call(this, 'toHaveURL', page, 'Page', async () => {
|
const locator = page.locator(':root');
|
||||||
return page.url();
|
return toMatchText.call(this, 'toHaveURL', locator, 'Locator', async (expected, isNot, timeout) => {
|
||||||
}, typeof expected === 'string' ? constructURLBasedOnBaseURL(baseURL, expected) : expected, options);
|
return await (locator as any)._expect('to.have.url', { expected, isNot, timeout });
|
||||||
|
}, expected, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toHaveValue(
|
export function toHaveValue(
|
||||||
|
|
@ -251,7 +241,7 @@ export function toHaveValue(
|
||||||
expected: string | RegExp,
|
expected: string | RegExp,
|
||||||
options?: { timeout?: number },
|
options?: { timeout?: number },
|
||||||
) {
|
) {
|
||||||
return toMatchText.call(this, 'toHaveValue', locator, 'Locator', async timeout => {
|
return toMatchText.call(this, 'toHaveValue', locator, 'Locator', async (expected, isNot, timeout) => {
|
||||||
return await locator.inputValue({ timeout });
|
return await (locator as any)._expect('to.have.value', { expected, isNot, timeout });
|
||||||
}, expected, options);
|
}, expected, options);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,14 +16,14 @@
|
||||||
|
|
||||||
import { currentTestInfo } from '../globals';
|
import { currentTestInfo } from '../globals';
|
||||||
import type { Expect } from '../types';
|
import type { Expect } from '../types';
|
||||||
import { expectType, pollUntilDeadline } from '../util';
|
import { expectType } from '../util';
|
||||||
|
|
||||||
export async function toBeTruthy<T>(
|
export async function toBeTruthy(
|
||||||
this: ReturnType<Expect['getState']>,
|
this: ReturnType<Expect['getState']>,
|
||||||
matcherName: string,
|
matcherName: string,
|
||||||
receiver: any,
|
receiver: any,
|
||||||
receiverType: string,
|
receiverType: string,
|
||||||
query: (timeout: number) => Promise<T>,
|
query: (isNot: boolean, timeout: number) => Promise<{ pass: boolean }>,
|
||||||
options: { timeout?: number } = {},
|
options: { timeout?: number } = {},
|
||||||
) {
|
) {
|
||||||
const testInfo = currentTestInfo();
|
const testInfo = currentTestInfo();
|
||||||
|
|
@ -36,14 +36,12 @@ export async function toBeTruthy<T>(
|
||||||
promise: this.promise,
|
promise: this.promise,
|
||||||
};
|
};
|
||||||
|
|
||||||
let received: T;
|
let defaultExpectTimeout = testInfo.project.expect?.timeout;
|
||||||
let pass = false;
|
if (typeof defaultExpectTimeout === 'undefined')
|
||||||
|
defaultExpectTimeout = 5000;
|
||||||
|
const timeout = options.timeout === 0 ? 0 : options.timeout || defaultExpectTimeout;
|
||||||
|
|
||||||
await pollUntilDeadline(testInfo, async remainingTime => {
|
const { pass } = await query(this.isNot, timeout);
|
||||||
received = await query(remainingTime);
|
|
||||||
pass = !!received;
|
|
||||||
return pass === !matcherOptions.isNot;
|
|
||||||
}, options.timeout, testInfo._testFinished);
|
|
||||||
|
|
||||||
const message = () => {
|
const message = () => {
|
||||||
return this.utils.matcherHint(matcherName, undefined, '', matcherOptions);
|
return this.utils.matcherHint(matcherName, undefined, '', matcherOptions);
|
||||||
|
|
|
||||||
|
|
@ -18,19 +18,20 @@ import {
|
||||||
printReceivedStringContainExpectedResult,
|
printReceivedStringContainExpectedResult,
|
||||||
printReceivedStringContainExpectedSubstring
|
printReceivedStringContainExpectedSubstring
|
||||||
} from 'expect/build/print';
|
} from 'expect/build/print';
|
||||||
import { isString } from '../../utils/utils';
|
import { ExpectedTextValue } from '../../protocol/channels';
|
||||||
|
import { isRegExp, isString } from '../../utils/utils';
|
||||||
import { currentTestInfo } from '../globals';
|
import { currentTestInfo } from '../globals';
|
||||||
import type { Expect } from '../types';
|
import type { Expect } from '../types';
|
||||||
import { expectType, pollUntilDeadline } from '../util';
|
import { expectType } from '../util';
|
||||||
|
|
||||||
export async function toMatchText(
|
export async function toMatchText(
|
||||||
this: ReturnType<Expect['getState']>,
|
this: ReturnType<Expect['getState']>,
|
||||||
matcherName: string,
|
matcherName: string,
|
||||||
receiver: any,
|
receiver: any,
|
||||||
receiverType: string,
|
receiverType: string,
|
||||||
query: (timeout: number) => Promise<string>,
|
query: (expected: ExpectedTextValue, isNot: boolean, timeout: number) => Promise<{ pass: boolean, received: string }>,
|
||||||
expected: string | RegExp,
|
expected: string | RegExp,
|
||||||
options: { timeout?: number, matchSubstring?: boolean, normalizeWhiteSpace?: boolean } = {},
|
options: { timeout?: number, matchSubstring?: boolean, normalizeWhiteSpace?: boolean, useInnerText?: boolean } = {},
|
||||||
) {
|
) {
|
||||||
const testInfo = currentTestInfo();
|
const testInfo = currentTestInfo();
|
||||||
if (!testInfo)
|
if (!testInfo)
|
||||||
|
|
@ -57,25 +58,21 @@ export async function toMatchText(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let received: string;
|
let defaultExpectTimeout = testInfo.project.expect?.timeout;
|
||||||
let pass = false;
|
if (typeof defaultExpectTimeout === 'undefined')
|
||||||
if (options.normalizeWhiteSpace && isString(expected))
|
defaultExpectTimeout = 5000;
|
||||||
expected = normalizeWhiteSpace(expected);
|
const timeout = options.timeout === 0 ? 0 : options.timeout || defaultExpectTimeout;
|
||||||
|
|
||||||
await pollUntilDeadline(testInfo, async remainingTime => {
|
const expectedValue: ExpectedTextValue = {
|
||||||
received = await query(remainingTime);
|
string: isString(expected) ? expected : undefined,
|
||||||
if (options.normalizeWhiteSpace && isString(expected))
|
regexSource: isRegExp(expected) ? expected.source : undefined,
|
||||||
received = normalizeWhiteSpace(received);
|
regexFlags: isRegExp(expected) ? expected.flags : undefined,
|
||||||
if (options.matchSubstring)
|
matchSubstring: options.matchSubstring,
|
||||||
pass = received.includes(expected as string);
|
normalizeWhiteSpace: options.normalizeWhiteSpace,
|
||||||
else if (typeof expected === 'string')
|
useInnerText: options.useInnerText,
|
||||||
pass = received === expected;
|
};
|
||||||
else
|
|
||||||
pass = expected.test(received);
|
|
||||||
|
|
||||||
return pass === !matcherOptions.isNot;
|
|
||||||
}, options.timeout, testInfo._testFinished);
|
|
||||||
|
|
||||||
|
const { pass, received } = await query(expectedValue, this.isNot, timeout);
|
||||||
const stringSubstring = options.matchSubstring ? 'substring' : 'string';
|
const stringSubstring = options.matchSubstring ? 'substring' : 'string';
|
||||||
const message = pass
|
const message = pass
|
||||||
? () =>
|
? () =>
|
||||||
|
|
@ -83,7 +80,7 @@ export async function toMatchText(
|
||||||
? this.utils.matcherHint(matcherName, undefined, undefined, matcherOptions) +
|
? this.utils.matcherHint(matcherName, undefined, undefined, matcherOptions) +
|
||||||
'\n\n' +
|
'\n\n' +
|
||||||
`Expected ${stringSubstring}: not ${this.utils.printExpected(expected)}\n` +
|
`Expected ${stringSubstring}: not ${this.utils.printExpected(expected)}\n` +
|
||||||
`Received string: ${printReceivedStringContainExpectedSubstring(
|
`Received string: ${printReceivedStringContainExpectedSubstring(
|
||||||
received,
|
received,
|
||||||
received.indexOf(expected),
|
received.indexOf(expected),
|
||||||
expected.length,
|
expected.length,
|
||||||
|
|
@ -91,7 +88,7 @@ export async function toMatchText(
|
||||||
: this.utils.matcherHint(matcherName, undefined, undefined, matcherOptions) +
|
: this.utils.matcherHint(matcherName, undefined, undefined, matcherOptions) +
|
||||||
'\n\n' +
|
'\n\n' +
|
||||||
`Expected pattern: not ${this.utils.printExpected(expected)}\n` +
|
`Expected pattern: not ${this.utils.printExpected(expected)}\n` +
|
||||||
`Received string: ${printReceivedStringContainExpectedResult(
|
`Received string: ${printReceivedStringContainExpectedResult(
|
||||||
received,
|
received,
|
||||||
typeof expected.exec === 'function'
|
typeof expected.exec === 'function'
|
||||||
? expected.exec(received)
|
? expected.exec(received)
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@ import type { TestError, Location } from './types';
|
||||||
import { default as minimatch } from 'minimatch';
|
import { default as minimatch } from 'minimatch';
|
||||||
import { errors } from '../..';
|
import { errors } from '../..';
|
||||||
import debug from 'debug';
|
import debug from 'debug';
|
||||||
|
import { isRegExp } from '../utils/utils';
|
||||||
|
|
||||||
export async function pollUntilDeadline(testInfo: TestInfoImpl, func: (remainingTime: number) => Promise<boolean>, pollTime: number | undefined, deadlinePromise: Promise<void>): Promise<void> {
|
export async function pollUntilDeadline(testInfo: TestInfoImpl, func: (remainingTime: number) => Promise<boolean>, pollTime: number | undefined, deadlinePromise: Promise<void>): Promise<void> {
|
||||||
let defaultExpectTimeout = testInfo.project.expect?.timeout;
|
let defaultExpectTimeout = testInfo.project.expect?.timeout;
|
||||||
|
|
@ -82,10 +83,6 @@ export function monotonicTime(): number {
|
||||||
return seconds * 1000 + (nanoseconds / 1000000 | 0);
|
return seconds * 1000 + (nanoseconds / 1000000 | 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isRegExp(e: any): e is RegExp {
|
|
||||||
return e && typeof e === 'object' && (e instanceof RegExp || Object.prototype.toString.call(e) === '[object RegExp]');
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Matcher = (value: string) => boolean;
|
export type Matcher = (value: string) => boolean;
|
||||||
|
|
||||||
export type FilePatternFilter = {
|
export type FilePatternFilter = {
|
||||||
|
|
|
||||||
|
|
@ -84,6 +84,34 @@ test('should support toHaveText w/ text', async ({ runInlineTest }) => {
|
||||||
expect(result.exitCode).toBe(1);
|
expect(result.exitCode).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should support toHaveText w/ not', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'a.test.ts': `
|
||||||
|
const { test } = pwt;
|
||||||
|
|
||||||
|
test('pass', async ({ page }) => {
|
||||||
|
await page.setContent('<div id=node>Text content</div>');
|
||||||
|
const locator = page.locator('#node');
|
||||||
|
await expect(locator).not.toHaveText('Text2');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fail', async ({ page }) => {
|
||||||
|
await page.setContent('<div id=node>Text content</div>');
|
||||||
|
const locator = page.locator('#node');
|
||||||
|
await expect(locator).not.toHaveText('Text content', { timeout: 100 });
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
}, { workers: 1 });
|
||||||
|
const output = stripAscii(result.output);
|
||||||
|
expect(output).toContain('Error: expect(received).not.toHaveText(expected)');
|
||||||
|
expect(output).toContain('Expected string: not "Text content"');
|
||||||
|
expect(output).toContain('Received string: "Text content');
|
||||||
|
expect(output).toContain('expect(locator).not.toHaveText');
|
||||||
|
expect(result.passed).toBe(1);
|
||||||
|
expect(result.failed).toBe(1);
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
test('should support toHaveText w/ array', async ({ runInlineTest }) => {
|
test('should support toHaveText w/ array', async ({ runInlineTest }) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'a.test.ts': `
|
'a.test.ts': `
|
||||||
|
|
@ -213,6 +241,41 @@ test('should support toHaveValue', async ({ runInlineTest }) => {
|
||||||
expect(result.exitCode).toBe(0);
|
expect(result.exitCode).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('should support toHaveValue regex', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'a.test.ts': `
|
||||||
|
const { test } = pwt;
|
||||||
|
|
||||||
|
test('pass', async ({ page }) => {
|
||||||
|
await page.setContent('<input id=node></input>');
|
||||||
|
const locator = page.locator('#node');
|
||||||
|
await locator.fill('Text content');
|
||||||
|
await expect(locator).toHaveValue(/Text/);
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
}, { workers: 1 });
|
||||||
|
expect(result.passed).toBe(1);
|
||||||
|
expect(result.exitCode).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should support toHaveValue failing', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'a.test.ts': `
|
||||||
|
const { test } = pwt;
|
||||||
|
|
||||||
|
test('pass', async ({ page }) => {
|
||||||
|
await page.setContent('<input id=node></input>');
|
||||||
|
const locator = page.locator('#node');
|
||||||
|
await locator.fill('Text content');
|
||||||
|
await expect(locator).toHaveValue(/Text2/, { timeout: 1000 });
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
}, { workers: 1 });
|
||||||
|
expect(result.passed).toBe(0);
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
expect(result.output).toContain('"Text content"');
|
||||||
|
});
|
||||||
|
|
||||||
test('should print expected/received before timeout', async ({ runInlineTest }) => {
|
test('should print expected/received before timeout', async ({ runInlineTest }) => {
|
||||||
const result = await runInlineTest({
|
const result = await runInlineTest({
|
||||||
'a.test.ts': `
|
'a.test.ts': `
|
||||||
|
|
|
||||||
|
|
@ -27,12 +27,6 @@ test('should support toBeChecked', async ({ runInlineTest }) => {
|
||||||
await expect(locator).toBeChecked();
|
await expect(locator).toBeChecked();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('pass not', async ({ page }) => {
|
|
||||||
await page.setContent('<input type=checkbox></input>');
|
|
||||||
const locator = page.locator('input');
|
|
||||||
await expect(locator).not.toBeChecked();
|
|
||||||
});
|
|
||||||
|
|
||||||
test('fail', async ({ page }) => {
|
test('fail', async ({ page }) => {
|
||||||
await page.setContent('<input type=checkbox></input>');
|
await page.setContent('<input type=checkbox></input>');
|
||||||
const locator = page.locator('input');
|
const locator = page.locator('input');
|
||||||
|
|
@ -43,7 +37,33 @@ test('should support toBeChecked', async ({ runInlineTest }) => {
|
||||||
const output = stripAscii(result.output);
|
const output = stripAscii(result.output);
|
||||||
expect(output).toContain('Error: expect(received).toBeChecked()');
|
expect(output).toContain('Error: expect(received).toBeChecked()');
|
||||||
expect(output).toContain('expect(locator).toBeChecked');
|
expect(output).toContain('expect(locator).toBeChecked');
|
||||||
expect(result.passed).toBe(2);
|
expect(result.passed).toBe(1);
|
||||||
|
expect(result.failed).toBe(1);
|
||||||
|
expect(result.exitCode).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should support toBeChecked w/ not', async ({ runInlineTest }) => {
|
||||||
|
const result = await runInlineTest({
|
||||||
|
'a.test.ts': `
|
||||||
|
const { test } = pwt;
|
||||||
|
|
||||||
|
test('pass not', async ({ page }) => {
|
||||||
|
await page.setContent('<input type=checkbox></input>');
|
||||||
|
const locator = page.locator('input');
|
||||||
|
await expect(locator).not.toBeChecked();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('fail not', async ({ page }) => {
|
||||||
|
await page.setContent('<input type=checkbox checked></input>');
|
||||||
|
const locator = page.locator('input');
|
||||||
|
await expect(locator).not.toBeChecked({ timeout: 1000 });
|
||||||
|
});
|
||||||
|
`,
|
||||||
|
}, { workers: 1 });
|
||||||
|
const output = stripAscii(result.output);
|
||||||
|
expect(output).toContain('Error: expect(received).not.toBeChecked()');
|
||||||
|
expect(output).toContain('expect(locator).not.toBeChecked');
|
||||||
|
expect(result.passed).toBe(1);
|
||||||
expect(result.failed).toBe(1);
|
expect(result.failed).toBe(1);
|
||||||
expect(result.exitCode).toBe(1);
|
expect(result.exitCode).toBe(1);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -229,9 +229,9 @@ test('should report expect steps', async ({ runInlineTest }) => {
|
||||||
`%% end {\"title\":\"browserContext.newPage\",\"category\":\"pw:api\"}`,
|
`%% end {\"title\":\"browserContext.newPage\",\"category\":\"pw:api\"}`,
|
||||||
`%% end {\"title\":\"Before Hooks\",\"category\":\"hook\",\"steps\":[{\"title\":\"browserContext.newPage\",\"category\":\"pw:api\"}]}`,
|
`%% end {\"title\":\"Before Hooks\",\"category\":\"hook\",\"steps\":[{\"title\":\"browserContext.newPage\",\"category\":\"pw:api\"}]}`,
|
||||||
`%% begin {\"title\":\"expect.not.toHaveTitle\",\"category\":\"expect\"}`,
|
`%% begin {\"title\":\"expect.not.toHaveTitle\",\"category\":\"expect\"}`,
|
||||||
`%% begin {\"title\":\"object.expect.toHaveTitle\",\"category\":\"pw:api\"}`,
|
`%% begin {\"title\":\"object.expect.toHaveTitle(:root)\",\"category\":\"pw:api\"}`,
|
||||||
`%% end {\"title\":\"object.expect.toHaveTitle\",\"category\":\"pw:api\"}`,
|
`%% end {\"title\":\"object.expect.toHaveTitle(:root)\",\"category\":\"pw:api\"}`,
|
||||||
`%% end {\"title\":\"expect.not.toHaveTitle\",\"category\":\"expect\",\"steps\":[{\"title\":\"object.expect.toHaveTitle\",\"category\":\"pw:api\"}]}`,
|
`%% end {\"title\":\"expect.not.toHaveTitle\",\"category\":\"expect\",\"steps\":[{\"title\":\"object.expect.toHaveTitle(:root)\",\"category\":\"pw:api\"}]}`,
|
||||||
`%% begin {\"title\":\"After Hooks\",\"category\":\"hook\"}`,
|
`%% begin {\"title\":\"After Hooks\",\"category\":\"hook\"}`,
|
||||||
`%% begin {\"title\":\"browserContext.close\",\"category\":\"pw:api\"}`,
|
`%% begin {\"title\":\"browserContext.close\",\"category\":\"pw:api\"}`,
|
||||||
`%% end {\"title\":\"browserContext.close\",\"category\":\"pw:api\"}`,
|
`%% end {\"title\":\"browserContext.close\",\"category\":\"pw:api\"}`,
|
||||||
|
|
|
||||||
|
|
@ -163,7 +163,7 @@ DEPS['src/server/'] = [
|
||||||
// No dependencies for code shared between node and page.
|
// No dependencies for code shared between node and page.
|
||||||
DEPS['src/server/common/'] = [];
|
DEPS['src/server/common/'] = [];
|
||||||
// Strict dependencies for injected code.
|
// Strict dependencies for injected code.
|
||||||
DEPS['src/server/injected/'] = ['src/server/common/'];
|
DEPS['src/server/injected/'] = ['src/server/common/', 'src/protocol/channels.ts'];
|
||||||
|
|
||||||
// Electron and Clank use chromium internally.
|
// Electron and Clank use chromium internally.
|
||||||
DEPS['src/server/android/'] = [...DEPS['src/server/'], 'src/server/chromium/', 'src/protocol/'];
|
DEPS['src/server/android/'] = [...DEPS['src/server/'], 'src/server/chromium/', 'src/protocol/'];
|
||||||
|
|
@ -193,7 +193,7 @@ DEPS['src/server/trace/recorder/'] = ['src/server/trace/common/', ...DEPS['src/s
|
||||||
DEPS['src/server/trace/viewer/'] = ['src/server/trace/common/', 'src/server/trace/recorder/', 'src/server/chromium/', ...DEPS['src/server/trace/common/']];
|
DEPS['src/server/trace/viewer/'] = ['src/server/trace/common/', 'src/server/trace/recorder/', 'src/server/chromium/', ...DEPS['src/server/trace/common/']];
|
||||||
|
|
||||||
// Playwright Test
|
// Playwright Test
|
||||||
DEPS['src/test/'] = ['src/test/**', 'src/utils/utils.ts', 'src/utils/**'];
|
DEPS['src/test/'] = ['src/test/**', 'src/utils/utils.ts', 'src/utils/**', 'src/protocol/channels.ts'];
|
||||||
DEPS['src/test/index.ts'] = [... DEPS['src/test/'], 'src/grid/gridClient.ts' ];
|
DEPS['src/test/index.ts'] = [... DEPS['src/test/'], 'src/grid/gridClient.ts' ];
|
||||||
|
|
||||||
// HTML report
|
// HTML report
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue