Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions e2e/mobilewright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ function resolveDriver(): DriverConfig {
const config: MobilewrightConfig = defineConfig({
testDir: './src',
testMatch: '**/*.test.ts',
trace: 'on',
retries: 0,
timeout: 60_000,
driver: resolveDriver(),
Expand Down
34 changes: 33 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion packages/mobilewright-core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@
],
"dependencies": {
"@mobilewright/protocol": "^0.0.1",
"sharp": "^0.34.5"
"sharp": "^0.34.5",
"yazl": "^3.3.1"
},
"devDependencies": {
"@types/yazl": "^3.3.1"
}
}
10 changes: 9 additions & 1 deletion packages/mobilewright-core/src/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type {
} from '@mobilewright/protocol';
import { Screen } from './screen.js';
import type { LocatorOptions } from './locator.js';
import type { Tracer } from './tracing.js';

export interface DeviceOptions {
locatorDefaults?: LocatorOptions;
Expand All @@ -20,12 +21,19 @@ export class Device {
private cleanupCallbacks: Array<() => Promise<void>> = [];
private _screen: Screen | null = null;
private readonly opts: DeviceOptions;
private _tracer: Tracer | null = null;

constructor(driver: MobilewrightDriver, opts: DeviceOptions = {}) {
this.driver = driver;
this.opts = opts;
}

setTracer(tracer: Tracer): void {
this._tracer = tracer;
tracer.setDriver(this.driver);
this._screen = null; // reset so next access picks up tracer
}

/** Register a callback to run on close(). Used by launchers for cleanup. */
onClose(callback: () => Promise<void>): void {
this.cleanupCallbacks.push(callback);
Expand All @@ -51,7 +59,7 @@ export class Device {
}

get screen(): Screen {
this._screen ??= new Screen(this.driver, this.opts.locatorDefaults);
this._screen ??= new Screen(this.driver, this.opts.locatorDefaults, this._tracer);
return this._screen;
}

Expand Down
101 changes: 65 additions & 36 deletions packages/mobilewright-core/src/expect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,74 +32,103 @@ class LocatorAssertions {
private readonly negated: boolean,
) {}

private async _wrapAssertion<T>(method: string, params: Record<string, unknown>, fn: () => Promise<T>): Promise<T> {
const tracer = this.locator._tracer;
if (!tracer) {
return fn();
}
const label = this.negated ? `not.${method}` : method;
return tracer.wrapAction('Expect', label, params, fn);
}

get not(): LocatorAssertions {
return new LocatorAssertions(this.locator, !this.negated);
}

async toBeVisible(opts?: ExpectOptions): Promise<void> {
await this.assertBoolean('visible', () => this.locator.isVisible({ timeout: 0 }), opts);
return this._wrapAssertion('toBeVisible', {}, async () => {
await this.assertBoolean('visible', () => this.locator.isVisible({ timeout: 0 }), opts);
});
}

async toBeHidden(opts?: ExpectOptions): Promise<void> {
await this.assertBoolean('hidden', async () => {
const visible = await this.locator.isVisible({ timeout: 0 });
return !visible;
}, opts);
return this._wrapAssertion('toBeHidden', {}, async () => {
await this.assertBoolean('hidden', async () => {
const visible = await this.locator.isVisible({ timeout: 0 });
return !visible;
}, opts);
});
}

async toBeEnabled(opts?: ExpectOptions): Promise<void> {
await this.assertBoolean('enabled', () => this.locator.isEnabled({ timeout: 0 }), opts);
return this._wrapAssertion('toBeEnabled', {}, async () => {
await this.assertBoolean('enabled', () => this.locator.isEnabled({ timeout: 0 }), opts);
});
}

async toBeDisabled(opts?: ExpectOptions): Promise<void> {
await this.assertBoolean('disabled', async () => {
const enabled = await this.locator.isEnabled({ timeout: 0 });
return !enabled;
}, opts);
return this._wrapAssertion('toBeDisabled', {}, async () => {
await this.assertBoolean('disabled', async () => {
const enabled = await this.locator.isEnabled({ timeout: 0 });
return !enabled;
}, opts);
});
}

async toBeSelected(opts?: ExpectOptions): Promise<void> {
await this.assertBoolean('selected', () => this.locator.isSelected({ timeout: 0 }), opts);
return this._wrapAssertion('toBeSelected', {}, async () => {
await this.assertBoolean('selected', () => this.locator.isSelected({ timeout: 0 }), opts);
});
}

async toBeFocused(opts?: ExpectOptions): Promise<void> {
await this.assertBoolean('focused', () => this.locator.isFocused({ timeout: 0 }), opts);
return this._wrapAssertion('toBeFocused', {}, async () => {
await this.assertBoolean('focused', () => this.locator.isFocused({ timeout: 0 }), opts);
});
}

async toBeChecked(opts?: ExpectOptions): Promise<void> {
await this.assertBoolean('checked', () => this.locator.isChecked({ timeout: 0 }), opts);
return this._wrapAssertion('toBeChecked', {}, async () => {
await this.assertBoolean('checked', () => this.locator.isChecked({ timeout: 0 }), opts);
});
}

async toHaveText(expected: string | RegExp, opts?: ExpectOptions): Promise<void> {
await this.assertText(
(text) => expected instanceof RegExp ? expected.test(text) : text === expected,
expected, opts,
);
return this._wrapAssertion('toHaveText', { expected: String(expected) }, async () => {
await this.assertText(
(text) => expected instanceof RegExp ? expected.test(text) : text === expected,
expected, opts,
);
});
}

async toContainText(expected: string, opts?: ExpectOptions): Promise<void> {
await this.assertText(
(text) => text.includes(expected),
expected, opts,
);
return this._wrapAssertion('toContainText', { expected }, async () => {
await this.assertText(
(text) => text.includes(expected),
expected, opts,
);
});
}

async toHaveValue(expected: string | RegExp, opts?: ExpectOptions): Promise<void> {
let lastValue = '';
await this.retryUntil(
async () => {
try { lastValue = await this.locator.getValue({ timeout: 0 }); } catch { lastValue = ''; }
return lastValue;
},
(value) => {
const matches = expected instanceof RegExp ? expected.test(value) : value === expected;
return this.negated ? !matches : matches;
},
opts?.timeout ?? DEFAULT_TIMEOUT,
() => this.negated
? `Expected element NOT to have value "${expected}", but got "${lastValue}"`
: `Expected element to have value "${expected}", but got "${lastValue}"`,
);
return this._wrapAssertion('toHaveValue', { expected: String(expected) }, async () => {
let lastValue = '';
await this.retryUntil(
async () => {
try { lastValue = await this.locator.getValue({ timeout: 0 }); } catch { lastValue = ''; }
return lastValue;
},
(value) => {
const matches = expected instanceof RegExp ? expected.test(value) : value === expected;
return this.negated ? !matches : matches;
},
opts?.timeout ?? DEFAULT_TIMEOUT,
() => this.negated
? `Expected element NOT to have value "${expected}", but got "${lastValue}"`
: `Expected element to have value "${expected}", but got "${lastValue}"`,
);
});
}

private async assertBoolean(
Expand Down
1 change: 1 addition & 0 deletions packages/mobilewright-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export { Device, type DeviceOptions } from './device.js';
export { expect, ExpectError, type ExpectOptions } from './expect.js';
export { queryAll, type LocatorStrategy } from './query-engine.js';
export { sleep } from './sleep.js';
export { Tracer } from './tracing.js';
Loading
Loading