@allwright.dev/vitest 0.1.13 → 0.1.14

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -164,3 +164,27 @@ Both locators must belong to the same page. `.not(other)` removes matching nodes
164
164
  The same exclusion API is available in the core web clients as TypeScript/Java
165
165
  `.not(other)`, Go `.Not(other)`, Rust `.not(&other)`, and Python `.not_(other)`.
166
166
  Native Android locator exclusion is not exposed by this web selector API.
167
+
168
+ ### Retrying state expectations
169
+
170
+ Assert against the page or locator so each attempt reads fresh state:
171
+
172
+ ```ts
173
+ await expect(page).toHaveURL('https://example.com/dashboard');
174
+ await expect(page).toHaveURL(/\/dashboard(?:\?|$)/, { timeoutMs: 10_000 });
175
+ await expect(page.locator('input')).toHaveValue(/ready/i);
176
+ await expect(page.locator('select')).toHaveSelectedOptions(['a', 'b']);
177
+ await expect(page.locator('select')).toHaveSelectedOptions([{ value: 'a', label: /Alpha/ }]);
178
+ await expect(page.locator('textarea')).toHaveSelectedText('selected words');
179
+ await expect(page.locator('input[type=checkbox]')).toBeChecked();
180
+ await expect(page.locator('h1')).toHaveText('Welcome');
181
+ await expect(page.locator('a')).toHaveAttribute('href', /dashboard/);
182
+ await expect(page.locator('button')).toHaveBoundingBox({ width: 100, height: 40 });
183
+ await expect(page.locator('input[type=radio]')).not().toBeChecked();
184
+ ```
185
+
186
+ Every element matcher also has a page form, such as `expect(page).toHaveValue('#name', 'Alice')` or `expect(page).toHaveAttribute('a', 'href', '/home')`. These state matchers are web-only; Android retains its text/count/visibility matchers.
187
+
188
+ All Allwright page/app/locator expectations retry until they match or the timeout expires, including `.not` and `.not()`. Defaults come from the shared assertion configuration, falling back to 5 seconds and a 100 ms interval. Override these with `{ timeoutMs, intervalMs }`. In-flight reads and nested command timeout hints are bounded by the remaining assertion budget; `timeoutMs: 0` makes one observation without polling. Ordinary assertions on already captured values, such as `expect(await page.url()).toBe(...)`, use standard Vitest behavior and do not re-read the page.
189
+
190
+ String URL expectations match the complete URL exactly; regex expectations match the current URL on every attempt without modifying the regex's `lastIndex`. Values, selected text, and attributes also accept strings or regexes. `null` asserts a missing attribute or unsupported textbox selection; an empty string is distinct. Selected-option arrays match in order and require the same number of options. Entries can be exact values, value regexes, or objects specifying value, label, and/or index. Bounding-box expectations compare only the supplied coordinates/dimensions exactly in viewport CSS pixels; `null` expects no visible box. `.not.toBeChecked()` expects an unchecked control. Missing elements and read errors cannot satisfy negation.
@@ -1,4 +1,4 @@
1
- import type { CommandOptions, Locator, MobileAndroidApp, MobileAndroidLocator, Page, WaitForSelectorOptions } from "@allwright.dev/core";
1
+ import type { BoundingBox, CommandOptions, Locator, MobileAndroidApp, MobileAndroidLocator, Page, WaitForSelectorOptions } from "@allwright.dev/core";
2
2
  export interface RetryExpectationOptions {
3
3
  timeoutMs?: number;
4
4
  intervalMs?: number;
@@ -9,22 +9,50 @@ export interface TextExpectationOptions extends RetryExpectationOptions {
9
9
  export interface VisibleExpectationOptions extends RetryExpectationOptions {
10
10
  command?: WaitForSelectorOptions;
11
11
  }
12
- export interface PageExpectMatchers {
13
- readonly not: PageExpectMatchers & (() => PageExpectMatchers);
12
+ export interface MobilePageExpectMatchers {
13
+ readonly not: MobilePageExpectMatchers & (() => MobilePageExpectMatchers);
14
14
  toHaveText(selector: string, expected: string | RegExp, options?: TextExpectationOptions): Promise<void>;
15
15
  toContainText(selector: string, expected: string | RegExp, options?: TextExpectationOptions): Promise<void>;
16
16
  toHaveCount(selector: string, expected: number, options?: RetryExpectationOptions): Promise<void>;
17
17
  toBeVisible(selector: string, options?: VisibleExpectationOptions): Promise<void>;
18
18
  }
19
- export interface LocatorExpectMatchers {
20
- readonly not: LocatorExpectMatchers & (() => LocatorExpectMatchers);
19
+ export interface MobileLocatorExpectMatchers {
20
+ readonly not: MobileLocatorExpectMatchers & (() => MobileLocatorExpectMatchers);
21
21
  toHaveText(expected: string | RegExp, options?: TextExpectationOptions): Promise<void>;
22
22
  toContainText(expected: string | RegExp, options?: TextExpectationOptions): Promise<void>;
23
23
  toHaveCount(expected: number, options?: RetryExpectationOptions): Promise<void>;
24
24
  toBeVisible(options?: VisibleExpectationOptions): Promise<void>;
25
25
  }
26
+ export type SelectedOptionExpectation = string | RegExp | {
27
+ value?: string | RegExp;
28
+ label?: string | RegExp;
29
+ index?: number;
30
+ };
31
+ export interface LocatorExpectMatchers extends MobileLocatorExpectMatchers {
32
+ readonly not: LocatorExpectMatchers & (() => LocatorExpectMatchers);
33
+ toHaveValue(expected: string | RegExp, options?: TextExpectationOptions): Promise<void>;
34
+ toHaveSelectedOptions(expected: SelectedOptionExpectation[], options?: TextExpectationOptions): Promise<void>;
35
+ toHaveSelectedText(expected: string | RegExp | null, options?: TextExpectationOptions): Promise<void>;
36
+ toBeChecked(options?: TextExpectationOptions): Promise<void>;
37
+ toHaveAttribute(name: string, expected: string | RegExp | null, options?: TextExpectationOptions): Promise<void>;
38
+ toHaveBoundingBox(expected: Partial<BoundingBox> | null, options?: TextExpectationOptions): Promise<void>;
39
+ }
40
+ export interface PageExpectMatchers extends MobilePageExpectMatchers {
41
+ readonly not: PageExpectMatchers & (() => PageExpectMatchers);
42
+ toHaveURL(expected: string | RegExp, options?: TextExpectationOptions): Promise<void>;
43
+ toHaveValue(selector: string, expected: string | RegExp, options?: TextExpectationOptions): Promise<void>;
44
+ toHaveSelectedOptions(selector: string, expected: SelectedOptionExpectation[], options?: TextExpectationOptions): Promise<void>;
45
+ toHaveSelectedText(selector: string, expected: string | RegExp | null, options?: TextExpectationOptions): Promise<void>;
46
+ toBeChecked(selector: string, options?: TextExpectationOptions): Promise<void>;
47
+ toHaveAttribute(selector: string, name: string, expected: string | RegExp | null, options?: TextExpectationOptions): Promise<void>;
48
+ toHaveBoundingBox(selector: string, expected: Partial<BoundingBox> | null, options?: TextExpectationOptions): Promise<void>;
49
+ }
26
50
  type Defaults = () => RetryExpectationOptions;
27
51
  type AnyLocator = Locator | MobileAndroidLocator;
28
- export declare function createLocatorExpect(locator: AnyLocator, defaults: Defaults, negated?: boolean): LocatorExpectMatchers;
29
- export declare function createPageExpect(page: Page | MobileAndroidApp, defaults: Defaults, negated?: boolean): PageExpectMatchers;
52
+ export declare function createLocatorExpect(locator: Locator, defaults: Defaults, negated?: boolean): LocatorExpectMatchers;
53
+ export declare function createLocatorExpect(locator: MobileAndroidLocator, defaults: Defaults, negated?: boolean): MobileLocatorExpectMatchers;
54
+ export declare function createLocatorExpect(locator: AnyLocator, defaults: Defaults, negated?: boolean): MobileLocatorExpectMatchers;
55
+ export declare function createPageExpect(page: Page, defaults: Defaults, negated?: boolean): PageExpectMatchers;
56
+ export declare function createPageExpect(page: MobileAndroidApp, defaults: Defaults, negated?: boolean): MobilePageExpectMatchers;
57
+ export declare function createPageExpect(page: Page | MobileAndroidApp, defaults: Defaults, negated?: boolean): MobilePageExpectMatchers;
30
58
  export {};
@@ -9,19 +9,41 @@ async function retryExpectation(callback, options, defaults) {
9
9
  throw new Error("Expectation timeoutMs and intervalMs must be finite non-negative numbers");
10
10
  }
11
11
  const deadline = Date.now() + timeoutMs;
12
+ let lastError;
12
13
  while (true) {
14
+ let timer;
13
15
  try {
14
- // The assertion owns retrying; do not let a nested command use the server's full default timeout.
15
- await callback({ timeoutMs: Math.max(1, Math.min(options.command?.timeoutMs ?? Math.max(1, intervalMs), deadline - Date.now())) });
16
+ // The assertion owns polling and bounds both the command hint and in-flight read.
17
+ const attempt = callback({ timeoutMs: Math.max(1, Math.min(options.command?.timeoutMs ?? Math.max(1, intervalMs), deadline - Date.now())) });
18
+ if (timeoutMs === 0)
19
+ await attempt; // One observation, without polling.
20
+ else
21
+ await Promise.race([
22
+ attempt,
23
+ new Promise((_, reject) => {
24
+ timer = setTimeout(() => reject(lastError ?? new Error(`Expectation timed out after ${timeoutMs}ms while reading state`)), Math.max(0, deadline - Date.now()));
25
+ }),
26
+ ]);
16
27
  return;
17
28
  }
18
29
  catch (error) {
19
- if (Date.now() + intervalMs >= deadline)
20
- throw error;
21
- await new Promise(resolve => setTimeout(resolve, intervalMs));
30
+ lastError = error;
22
31
  }
32
+ finally {
33
+ if (timer !== undefined)
34
+ clearTimeout(timer);
35
+ }
36
+ const remaining = deadline - Date.now();
37
+ if (remaining <= 0)
38
+ throw lastError;
39
+ await new Promise(resolve => setTimeout(resolve, Math.min(intervalMs, remaining)));
40
+ if (Date.now() >= deadline)
41
+ throw lastError;
23
42
  }
24
43
  }
44
+ function expectedText(expected) {
45
+ return expected instanceof RegExp ? vitestExpect.stringMatching(new RegExp(expected.source, expected.flags)) : expected;
46
+ }
25
47
  async function isVisible(locator, command) {
26
48
  if ("filter" in locator) {
27
49
  return (await locator.filter({ visible: true }).count(command)).count > 0;
@@ -41,7 +63,7 @@ async function isVisible(locator, command) {
41
63
  }
42
64
  export function createLocatorExpect(locator, defaults, negated = false) {
43
65
  const assertion = (value) => negated ? vitestExpect(value).not : vitestExpect(value);
44
- return {
66
+ const common = {
45
67
  get not() { return callableMatchers(createLocatorExpect(locator, defaults, !negated)); },
46
68
  async toHaveText(expected, options = {}) {
47
69
  await retryExpectation(async (command) => {
@@ -72,14 +94,76 @@ export function createLocatorExpect(locator, defaults, negated = false) {
72
94
  }, options, defaults());
73
95
  },
74
96
  };
97
+ if (!("inputValue" in locator))
98
+ return common;
99
+ const web = {
100
+ toHaveText: common.toHaveText,
101
+ toContainText: common.toContainText,
102
+ toHaveCount: common.toHaveCount,
103
+ toBeVisible: common.toBeVisible,
104
+ get not() { return callableMatchers(createLocatorExpect(locator, defaults, !negated)); },
105
+ async toHaveValue(expected, options = {}) {
106
+ await retryExpectation(async (command) => assertion(await locator.inputValue(command)).toEqual(expectedText(expected)), options, defaults());
107
+ },
108
+ async toHaveSelectedOptions(expected, options = {}) {
109
+ await retryExpectation(async (command) => {
110
+ const wanted = expected.map(option => {
111
+ if (typeof option === "string" || option instanceof RegExp)
112
+ return vitestExpect.objectContaining({ value: expectedText(option) });
113
+ return vitestExpect.objectContaining({
114
+ ...(option.value !== undefined ? { value: expectedText(option.value) } : {}),
115
+ ...(option.label !== undefined ? { label: expectedText(option.label) } : {}),
116
+ ...(option.index !== undefined ? { index: option.index } : {}),
117
+ });
118
+ });
119
+ assertion(await locator.selectedOptions(command)).toEqual(wanted);
120
+ }, options, defaults());
121
+ },
122
+ async toHaveSelectedText(expected, options = {}) {
123
+ await retryExpectation(async (command) => assertion(await locator.selectedText(command)).toEqual(expectedText(expected)), options, defaults());
124
+ },
125
+ async toBeChecked(options = {}) {
126
+ await retryExpectation(async (command) => assertion(await locator.isChecked(command)).toBe(true), options, defaults());
127
+ },
128
+ async toHaveAttribute(name, expected, options = {}) {
129
+ await retryExpectation(async (command) => assertion(await locator.getAttribute(name, command)).toEqual(expectedText(expected)), options, defaults());
130
+ },
131
+ async toHaveBoundingBox(expected, options = {}) {
132
+ await retryExpectation(async (command) => assertion(await locator.boundingBox(command)).toEqual(expected === null ? null : vitestExpect.objectContaining(expected)), options, defaults());
133
+ },
134
+ };
135
+ return web;
75
136
  }
76
137
  export function createPageExpect(page, defaults, negated = false) {
77
138
  const matcher = (selector) => createLocatorExpect(page.locator(selector), defaults, negated);
78
- return {
139
+ const common = {
79
140
  get not() { return callableMatchers(createPageExpect(page, defaults, !negated)); },
80
141
  toHaveText: (selector, expected, options) => matcher(selector).toHaveText(expected, options),
81
142
  toContainText: (selector, expected, options) => matcher(selector).toContainText(expected, options),
82
143
  toHaveCount: (selector, expected, options) => matcher(selector).toHaveCount(expected, options),
83
144
  toBeVisible: (selector, options) => matcher(selector).toBeVisible(options),
84
145
  };
146
+ if (!("url" in page))
147
+ return common;
148
+ const webMatcher = (selector) => createLocatorExpect(page.locator(selector), defaults, negated);
149
+ const web = {
150
+ toHaveText: common.toHaveText,
151
+ toContainText: common.toContainText,
152
+ toHaveCount: common.toHaveCount,
153
+ toBeVisible: common.toBeVisible,
154
+ get not() { return callableMatchers(createPageExpect(page, defaults, !negated)); },
155
+ async toHaveURL(expected, options = {}) {
156
+ await retryExpectation(async (command) => {
157
+ const url = await page.url(command);
158
+ (negated ? vitestExpect(url).not : vitestExpect(url)).toEqual(expectedText(expected));
159
+ }, options, defaults());
160
+ },
161
+ toHaveValue: (selector, expected, options) => webMatcher(selector).toHaveValue(expected, options),
162
+ toHaveSelectedOptions: (selector, expected, options) => webMatcher(selector).toHaveSelectedOptions(expected, options),
163
+ toHaveSelectedText: (selector, expected, options) => webMatcher(selector).toHaveSelectedText(expected, options),
164
+ toBeChecked: (selector, options) => webMatcher(selector).toBeChecked(options),
165
+ toHaveAttribute: (selector, name, expected, options) => webMatcher(selector).toHaveAttribute(name, expected, options),
166
+ toHaveBoundingBox: (selector, expected, options) => webMatcher(selector).toHaveBoundingBox(expected, options),
167
+ };
168
+ return web;
85
169
  }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import type { PageExpectMatchers, LocatorExpectMatchers } from "./expectations.js";
2
- export type { RetryExpectationOptions, TextExpectationOptions, VisibleExpectationOptions, PageExpectMatchers, LocatorExpectMatchers } from "./expectations.js";
1
+ import type { PageExpectMatchers, LocatorExpectMatchers, MobilePageExpectMatchers, MobileLocatorExpectMatchers } from "./expectations.js";
2
+ export type { RetryExpectationOptions, TextExpectationOptions, VisibleExpectationOptions, SelectedOptionExpectation, PageExpectMatchers, LocatorExpectMatchers, MobilePageExpectMatchers, MobileLocatorExpectMatchers } from "./expectations.js";
3
3
  import { type MobileAndroidConnectOptions, type MobileAndroidDevice, type MobileAndroidLaunchOptions, type MobileAndroidApp, type Browser, type BrowserKind, type LaunchOptions, type Locator, type MobileAndroidLocator, type Page } from "@allwright.dev/core";
4
4
  import { expect as vitestExpect, type Assertion } from "vitest";
5
5
  export interface AllwrightVitestOptions {
@@ -38,8 +38,8 @@ type VitestExpect = typeof vitestExpect;
38
38
  interface AllwrightExpect extends VitestExpect {
39
39
  (actual: Page): PageExpectMatchers;
40
40
  (actual: Locator): LocatorExpectMatchers;
41
- (actual: MobileAndroidApp): PageExpectMatchers;
42
- (actual: MobileAndroidLocator): LocatorExpectMatchers;
41
+ (actual: MobileAndroidApp): MobilePageExpectMatchers;
42
+ (actual: MobileAndroidLocator): MobileLocatorExpectMatchers;
43
43
  <T>(actual: T): Assertion<T>;
44
44
  }
45
45
  export declare const expect: AllwrightExpect;
package/dist/index.js CHANGED
@@ -80,6 +80,24 @@ function createLazyLocator(pageResource, selectorFactory) {
80
80
  async press(key, options) {
81
81
  return (await pageResource.get()).locator(await selectorFactory()).press(key, options);
82
82
  },
83
+ async inputValue(options) {
84
+ return (await pageResource.get()).locator(await selectorFactory()).inputValue(options);
85
+ },
86
+ async selectedOptions(options) {
87
+ return (await pageResource.get()).locator(await selectorFactory()).selectedOptions(options);
88
+ },
89
+ async selectedText(options) {
90
+ return (await pageResource.get()).locator(await selectorFactory()).selectedText(options);
91
+ },
92
+ async isChecked(options) {
93
+ return (await pageResource.get()).locator(await selectorFactory()).isChecked(options);
94
+ },
95
+ async getAttribute(name, options) {
96
+ return (await pageResource.get()).locator(await selectorFactory()).getAttribute(name, options);
97
+ },
98
+ async boundingBox(options) {
99
+ return (await pageResource.get()).locator(await selectorFactory()).boundingBox(options);
100
+ },
83
101
  async textContent(options) {
84
102
  return (await pageResource.get()).locator(await selectorFactory()).textContent(options);
85
103
  },
@@ -172,6 +190,27 @@ function createLazyPage(pageResource) {
172
190
  async press(selector, key, options) {
173
191
  return (await pageResource.get()).press(selector, key, options);
174
192
  },
193
+ async url(options) {
194
+ return (await pageResource.get()).url(options);
195
+ },
196
+ async inputValue(selector, options) {
197
+ return (await pageResource.get()).inputValue(selector, options);
198
+ },
199
+ async selectedOptions(selector, options) {
200
+ return (await pageResource.get()).selectedOptions(selector, options);
201
+ },
202
+ async selectedText(selector, options) {
203
+ return (await pageResource.get()).selectedText(selector, options);
204
+ },
205
+ async isChecked(selector, options) {
206
+ return (await pageResource.get()).isChecked(selector, options);
207
+ },
208
+ async getAttribute(selector, name, options) {
209
+ return (await pageResource.get()).getAttribute(selector, name, options);
210
+ },
211
+ async boundingBox(selector, options) {
212
+ return (await pageResource.get()).boundingBox(selector, options);
213
+ },
175
214
  async textContent(selector, options) {
176
215
  return (await pageResource.get()).textContent(selector, options);
177
216
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@allwright.dev/vitest",
3
- "version": "0.1.13",
3
+ "version": "0.1.14",
4
4
  "description": "Vitest fixtures for the allwright TypeScript client.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -47,7 +47,7 @@
47
47
  "prepublishOnly": "bun run build"
48
48
  },
49
49
  "dependencies": {
50
- "@allwright.dev/core": "0.1.13"
50
+ "@allwright.dev/core": "0.1.14"
51
51
  },
52
52
  "devDependencies": {
53
53
  "typescript": "^5.9.2",