sentienceapi 0.93.0 → 0.94.0
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/dist/asserts/expect.d.ts +159 -0
- package/dist/asserts/expect.d.ts.map +1 -0
- package/dist/asserts/expect.js +547 -0
- package/dist/asserts/expect.js.map +1 -0
- package/dist/asserts/index.d.ts +58 -0
- package/dist/asserts/index.d.ts.map +1 -0
- package/dist/asserts/index.js +70 -0
- package/dist/asserts/index.js.map +1 -0
- package/dist/asserts/query.d.ts +199 -0
- package/dist/asserts/query.d.ts.map +1 -0
- package/dist/asserts/query.js +288 -0
- package/dist/asserts/query.js.map +1 -0
- package/dist/backends/actions.d.ts +118 -0
- package/dist/backends/actions.d.ts.map +1 -0
- package/dist/backends/actions.js +262 -0
- package/dist/backends/actions.js.map +1 -0
- package/dist/backends/browser-use-adapter.d.ts +131 -0
- package/dist/backends/browser-use-adapter.d.ts.map +1 -0
- package/dist/backends/browser-use-adapter.js +219 -0
- package/dist/backends/browser-use-adapter.js.map +1 -0
- package/dist/backends/cdp-backend.d.ts +66 -0
- package/dist/backends/cdp-backend.d.ts.map +1 -0
- package/dist/backends/cdp-backend.js +273 -0
- package/dist/backends/cdp-backend.js.map +1 -0
- package/dist/backends/index.d.ts +80 -0
- package/dist/backends/index.d.ts.map +1 -0
- package/dist/backends/index.js +100 -0
- package/dist/backends/index.js.map +1 -0
- package/dist/backends/protocol.d.ts +156 -0
- package/dist/backends/protocol.d.ts.map +1 -0
- package/dist/backends/protocol.js +16 -0
- package/dist/backends/protocol.js.map +1 -0
- package/dist/backends/sentience-context.d.ts +136 -0
- package/dist/backends/sentience-context.d.ts.map +1 -0
- package/dist/backends/sentience-context.js +354 -0
- package/dist/backends/sentience-context.js.map +1 -0
- package/dist/backends/snapshot.d.ts +180 -0
- package/dist/backends/snapshot.d.ts.map +1 -0
- package/dist/backends/snapshot.js +308 -0
- package/dist/backends/snapshot.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +25 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- package/src/extension/release.json +1 -1
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Expectation builder for assertion DSL.
|
|
3
|
+
*
|
|
4
|
+
* This module provides the expect() builder that creates fluent assertions
|
|
5
|
+
* which compile to existing Predicate objects.
|
|
6
|
+
*
|
|
7
|
+
* Key classes:
|
|
8
|
+
* - ExpectBuilder: Fluent builder for element-based assertions
|
|
9
|
+
* - EventuallyConfig: Configuration for .eventually() retry logic
|
|
10
|
+
*
|
|
11
|
+
* The expect() function is the main entry point. It returns a builder that
|
|
12
|
+
* can be chained with matchers:
|
|
13
|
+
* expect(E({ role: "button" })).toExist()
|
|
14
|
+
* expect(E({ textContains: "Error" })).notToExist()
|
|
15
|
+
* expect.textPresent("Welcome")
|
|
16
|
+
*
|
|
17
|
+
* All builders compile to Predicate functions compatible with AgentRuntime.assert().
|
|
18
|
+
*/
|
|
19
|
+
import { Predicate, AssertOutcome, AssertContext } from '../verification';
|
|
20
|
+
import { ElementQuery, MultiQuery, MultiTextPredicate } from './query';
|
|
21
|
+
/**
|
|
22
|
+
* Configuration for .eventually() retry logic.
|
|
23
|
+
*/
|
|
24
|
+
export interface EventuallyConfig {
|
|
25
|
+
/** Max time to wait (milliseconds, default 10000) */
|
|
26
|
+
timeout?: number;
|
|
27
|
+
/** Interval between retries (milliseconds, default 200) */
|
|
28
|
+
poll?: number;
|
|
29
|
+
/** Max number of retry attempts (default 3) */
|
|
30
|
+
maxRetries?: number;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Fluent builder for element-based assertions.
|
|
34
|
+
*
|
|
35
|
+
* Created by expect(E(...)) or expect(inDominantList().nth(k)).
|
|
36
|
+
*
|
|
37
|
+
* Methods return Predicate functions that can be passed to runtime.assert().
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* expect(E({ role: "button" })).toExist()
|
|
41
|
+
* expect(E({ textContains: "Error" })).notToExist()
|
|
42
|
+
* expect(E({ role: "link" })).toBeVisible()
|
|
43
|
+
*/
|
|
44
|
+
export declare class ExpectBuilder {
|
|
45
|
+
private _query;
|
|
46
|
+
constructor(query: ElementQuery | MultiQuery | MultiTextPredicate);
|
|
47
|
+
/**
|
|
48
|
+
* Assert that at least one element matches the query.
|
|
49
|
+
*
|
|
50
|
+
* @returns Predicate function for use with runtime.assert()
|
|
51
|
+
*
|
|
52
|
+
* @example
|
|
53
|
+
* await runtime.assert(
|
|
54
|
+
* expect(E({ role: "button", textContains: "Save" })).toExist(),
|
|
55
|
+
* "save_button_exists"
|
|
56
|
+
* );
|
|
57
|
+
*/
|
|
58
|
+
toExist(): Predicate;
|
|
59
|
+
/**
|
|
60
|
+
* Assert that NO elements match the query.
|
|
61
|
+
*
|
|
62
|
+
* Useful for asserting absence of error messages, loading indicators, etc.
|
|
63
|
+
*
|
|
64
|
+
* @returns Predicate function for use with runtime.assert()
|
|
65
|
+
*
|
|
66
|
+
* @example
|
|
67
|
+
* await runtime.assert(
|
|
68
|
+
* expect(E({ textContains: "Error" })).notToExist(),
|
|
69
|
+
* "no_error_message"
|
|
70
|
+
* );
|
|
71
|
+
*/
|
|
72
|
+
notToExist(): Predicate;
|
|
73
|
+
/**
|
|
74
|
+
* Assert that element exists AND is visible (in_viewport=true, is_occluded=false).
|
|
75
|
+
*
|
|
76
|
+
* @returns Predicate function for use with runtime.assert()
|
|
77
|
+
*
|
|
78
|
+
* @example
|
|
79
|
+
* await runtime.assert(
|
|
80
|
+
* expect(E({ textContains: "Checkout" })).toBeVisible(),
|
|
81
|
+
* "checkout_button_visible"
|
|
82
|
+
* );
|
|
83
|
+
*/
|
|
84
|
+
toBeVisible(): Predicate;
|
|
85
|
+
/**
|
|
86
|
+
* Assert that element's text contains the specified substring.
|
|
87
|
+
*
|
|
88
|
+
* @param text - Substring to search for (case-insensitive)
|
|
89
|
+
* @returns Predicate function for use with runtime.assert()
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* await runtime.assert(
|
|
93
|
+
* expect(inDominantList().nth(0)).toHaveTextContains("Show HN"),
|
|
94
|
+
* "first_item_is_show_hn"
|
|
95
|
+
* );
|
|
96
|
+
*/
|
|
97
|
+
toHaveTextContains(text: string): Predicate;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Main entry point for the assertion DSL.
|
|
101
|
+
*
|
|
102
|
+
* Use as a function to create element-based assertions:
|
|
103
|
+
* expect(E({ role: "button" })).toExist()
|
|
104
|
+
*
|
|
105
|
+
* Use static methods for global assertions:
|
|
106
|
+
* expect.textPresent("Welcome")
|
|
107
|
+
* expect.noText("Error")
|
|
108
|
+
*/
|
|
109
|
+
export declare const expect: ((query: ElementQuery | MultiQuery | MultiTextPredicate) => ExpectBuilder) & {
|
|
110
|
+
textPresent: (text: string) => Predicate;
|
|
111
|
+
noText: (text: string) => Predicate;
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* Wrapper that adds retry logic to a predicate.
|
|
115
|
+
*
|
|
116
|
+
* Created by withEventually(). Provides an async evaluate() method
|
|
117
|
+
* that retries the predicate with fresh snapshots.
|
|
118
|
+
*
|
|
119
|
+
* Note: TypeScript uses milliseconds for timeout/poll.
|
|
120
|
+
*/
|
|
121
|
+
export declare class EventuallyWrapper {
|
|
122
|
+
private _predicate;
|
|
123
|
+
private _config;
|
|
124
|
+
constructor(predicate: Predicate, config?: EventuallyConfig);
|
|
125
|
+
/**
|
|
126
|
+
* Evaluate predicate with retry logic.
|
|
127
|
+
*
|
|
128
|
+
* @param ctx - Initial assertion context
|
|
129
|
+
* @param snapshotFn - Async function to take fresh snapshots
|
|
130
|
+
* @returns Promise resolving to AssertOutcome
|
|
131
|
+
*/
|
|
132
|
+
evaluate(ctx: AssertContext, snapshotFn: () => Promise<AssertContext['snapshot']>): Promise<AssertOutcome>;
|
|
133
|
+
private sleep;
|
|
134
|
+
/** Get the configured timeout in milliseconds */
|
|
135
|
+
get timeout(): number;
|
|
136
|
+
/** Get the configured poll interval in milliseconds */
|
|
137
|
+
get poll(): number;
|
|
138
|
+
/** Get the configured max retries */
|
|
139
|
+
get maxRetries(): number;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Wrap a predicate with retry logic.
|
|
143
|
+
*
|
|
144
|
+
* This is the TypeScript API for .eventually(). Returns a wrapper
|
|
145
|
+
* that provides an async evaluate() method for use with the runtime.
|
|
146
|
+
*
|
|
147
|
+
* @param predicate - Predicate to wrap
|
|
148
|
+
* @param config - Retry configuration (timeout/poll in milliseconds)
|
|
149
|
+
* @returns EventuallyWrapper with async evaluate() method
|
|
150
|
+
*
|
|
151
|
+
* @example
|
|
152
|
+
* const wrapper = withEventually(
|
|
153
|
+
* expect(E({ role: "button" })).toExist(),
|
|
154
|
+
* { timeout: 5000, maxRetries: 10 }
|
|
155
|
+
* );
|
|
156
|
+
* const result = await wrapper.evaluate(ctx, runtime.snapshot);
|
|
157
|
+
*/
|
|
158
|
+
export declare function withEventually(predicate: Predicate, config?: EventuallyConfig): EventuallyWrapper;
|
|
159
|
+
//# sourceMappingURL=expect.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"expect.d.ts","sourceRoot":"","sources":["../../src/asserts/expect.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,SAAS,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;AAC1E,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAOvE;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B,qDAAqD;IACrD,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2DAA2D;IAC3D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,+CAA+C;IAC/C,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAoCD;;;;;;;;;;;GAWG;AACH,qBAAa,aAAa;IACxB,OAAO,CAAC,MAAM,CAAiD;gBAEnD,KAAK,EAAE,YAAY,GAAG,UAAU,GAAG,kBAAkB;IAIjE;;;;;;;;;;OAUG;IACH,OAAO,IAAI,SAAS;IA+BpB;;;;;;;;;;;;OAYG;IACH,UAAU,IAAI,SAAS;IAiCvB;;;;;;;;;;OAUG;IACH,WAAW,IAAI,SAAS;IAgDxB;;;;;;;;;;;OAWG;IACH,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS;CA8C5C;AAqID;;;;;;;;;GASG;AACH,eAAO,MAAM,MAAM,WACT,YAAY,GAAG,UAAU,GAAG,kBAAkB;wBAEhC,MAAM;mBACX,MAAM;CAExB,CAAC;AAEF;;;;;;;GAOG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,UAAU,CAAY;IAC9B,OAAO,CAAC,OAAO,CAA6B;gBAEhC,SAAS,EAAE,SAAS,EAAE,MAAM,GAAE,gBAAqB;IAS/D;;;;;;OAMG;IACG,QAAQ,CACZ,GAAG,EAAE,aAAa,EAClB,UAAU,EAAE,MAAM,OAAO,CAAC,aAAa,CAAC,UAAU,CAAC,CAAC,GACnD,OAAO,CAAC,aAAa,CAAC;IA8EzB,OAAO,CAAC,KAAK;IAIb,iDAAiD;IACjD,IAAI,OAAO,IAAI,MAAM,CAEpB;IAED,uDAAuD;IACvD,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,qCAAqC;IACrC,IAAI,UAAU,IAAI,MAAM,CAEvB;CACF;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,cAAc,CAAC,SAAS,EAAE,SAAS,EAAE,MAAM,CAAC,EAAE,gBAAgB,GAAG,iBAAiB,CAEjG"}
|
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Expectation builder for assertion DSL.
|
|
4
|
+
*
|
|
5
|
+
* This module provides the expect() builder that creates fluent assertions
|
|
6
|
+
* which compile to existing Predicate objects.
|
|
7
|
+
*
|
|
8
|
+
* Key classes:
|
|
9
|
+
* - ExpectBuilder: Fluent builder for element-based assertions
|
|
10
|
+
* - EventuallyConfig: Configuration for .eventually() retry logic
|
|
11
|
+
*
|
|
12
|
+
* The expect() function is the main entry point. It returns a builder that
|
|
13
|
+
* can be chained with matchers:
|
|
14
|
+
* expect(E({ role: "button" })).toExist()
|
|
15
|
+
* expect(E({ textContains: "Error" })).notToExist()
|
|
16
|
+
* expect.textPresent("Welcome")
|
|
17
|
+
*
|
|
18
|
+
* All builders compile to Predicate functions compatible with AgentRuntime.assert().
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.EventuallyWrapper = exports.expect = exports.ExpectBuilder = void 0;
|
|
22
|
+
exports.withEventually = withEventually;
|
|
23
|
+
const query_1 = require("./query");
|
|
24
|
+
// Default values for .eventually()
|
|
25
|
+
const DEFAULT_TIMEOUT = 10000; // milliseconds
|
|
26
|
+
const DEFAULT_POLL = 200; // milliseconds
|
|
27
|
+
const DEFAULT_MAX_RETRIES = 3;
|
|
28
|
+
/**
|
|
29
|
+
* Convert query to a serializable object for debugging.
|
|
30
|
+
*/
|
|
31
|
+
function queryToDict(query) {
|
|
32
|
+
if (query instanceof query_1.ElementQuery) {
|
|
33
|
+
const result = {};
|
|
34
|
+
if (query.role)
|
|
35
|
+
result.role = query.role;
|
|
36
|
+
if (query.name)
|
|
37
|
+
result.name = query.name;
|
|
38
|
+
if (query.text)
|
|
39
|
+
result.text = query.text;
|
|
40
|
+
if (query.textContains)
|
|
41
|
+
result.textContains = query.textContains;
|
|
42
|
+
if (query.hrefContains)
|
|
43
|
+
result.hrefContains = query.hrefContains;
|
|
44
|
+
if (query.inViewport !== undefined)
|
|
45
|
+
result.inViewport = query.inViewport;
|
|
46
|
+
if (query.occluded !== undefined)
|
|
47
|
+
result.occluded = query.occluded;
|
|
48
|
+
if (query.group)
|
|
49
|
+
result.group = query.group;
|
|
50
|
+
if (query.inDominantGroup !== undefined)
|
|
51
|
+
result.inDominantGroup = query.inDominantGroup;
|
|
52
|
+
if (query._groupIndex !== undefined)
|
|
53
|
+
result.groupIndex = query._groupIndex;
|
|
54
|
+
if (query._fromDominantList)
|
|
55
|
+
result.fromDominantList = true;
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
else if (query instanceof query_1.MultiQuery) {
|
|
59
|
+
return { type: 'multi', limit: query.limit };
|
|
60
|
+
}
|
|
61
|
+
else if (typeof query === 'object' &&
|
|
62
|
+
query !== null &&
|
|
63
|
+
'checkType' in query &&
|
|
64
|
+
'text' in query &&
|
|
65
|
+
'multiQuery' in query) {
|
|
66
|
+
return { type: 'multi_text', text: query.text, checkType: query.checkType };
|
|
67
|
+
}
|
|
68
|
+
return { type: String(typeof query) };
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Fluent builder for element-based assertions.
|
|
72
|
+
*
|
|
73
|
+
* Created by expect(E(...)) or expect(inDominantList().nth(k)).
|
|
74
|
+
*
|
|
75
|
+
* Methods return Predicate functions that can be passed to runtime.assert().
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* expect(E({ role: "button" })).toExist()
|
|
79
|
+
* expect(E({ textContains: "Error" })).notToExist()
|
|
80
|
+
* expect(E({ role: "link" })).toBeVisible()
|
|
81
|
+
*/
|
|
82
|
+
class ExpectBuilder {
|
|
83
|
+
constructor(query) {
|
|
84
|
+
this._query = query;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Assert that at least one element matches the query.
|
|
88
|
+
*
|
|
89
|
+
* @returns Predicate function for use with runtime.assert()
|
|
90
|
+
*
|
|
91
|
+
* @example
|
|
92
|
+
* await runtime.assert(
|
|
93
|
+
* expect(E({ role: "button", textContains: "Save" })).toExist(),
|
|
94
|
+
* "save_button_exists"
|
|
95
|
+
* );
|
|
96
|
+
*/
|
|
97
|
+
toExist() {
|
|
98
|
+
const query = this._query;
|
|
99
|
+
return (ctx) => {
|
|
100
|
+
const snap = ctx.snapshot;
|
|
101
|
+
if (!snap) {
|
|
102
|
+
return {
|
|
103
|
+
passed: false,
|
|
104
|
+
reason: 'no snapshot available',
|
|
105
|
+
details: { query: queryToDict(query) },
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
if (query instanceof query_1.ElementQuery) {
|
|
109
|
+
const matches = query.findAll(snap);
|
|
110
|
+
const ok = matches.length > 0;
|
|
111
|
+
return {
|
|
112
|
+
passed: ok,
|
|
113
|
+
reason: ok ? '' : `no elements matched query: ${JSON.stringify(queryToDict(query))}`,
|
|
114
|
+
details: { query: queryToDict(query), matched: matches.length },
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
passed: false,
|
|
119
|
+
reason: 'toExist() requires ElementQuery',
|
|
120
|
+
details: {},
|
|
121
|
+
};
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Assert that NO elements match the query.
|
|
126
|
+
*
|
|
127
|
+
* Useful for asserting absence of error messages, loading indicators, etc.
|
|
128
|
+
*
|
|
129
|
+
* @returns Predicate function for use with runtime.assert()
|
|
130
|
+
*
|
|
131
|
+
* @example
|
|
132
|
+
* await runtime.assert(
|
|
133
|
+
* expect(E({ textContains: "Error" })).notToExist(),
|
|
134
|
+
* "no_error_message"
|
|
135
|
+
* );
|
|
136
|
+
*/
|
|
137
|
+
notToExist() {
|
|
138
|
+
const query = this._query;
|
|
139
|
+
return (ctx) => {
|
|
140
|
+
const snap = ctx.snapshot;
|
|
141
|
+
if (!snap) {
|
|
142
|
+
return {
|
|
143
|
+
passed: false,
|
|
144
|
+
reason: 'no snapshot available',
|
|
145
|
+
details: { query: queryToDict(query) },
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
if (query instanceof query_1.ElementQuery) {
|
|
149
|
+
const matches = query.findAll(snap);
|
|
150
|
+
const ok = matches.length === 0;
|
|
151
|
+
return {
|
|
152
|
+
passed: ok,
|
|
153
|
+
reason: ok
|
|
154
|
+
? ''
|
|
155
|
+
: `found ${matches.length} elements matching: ${JSON.stringify(queryToDict(query))}`,
|
|
156
|
+
details: { query: queryToDict(query), matched: matches.length },
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
return {
|
|
160
|
+
passed: false,
|
|
161
|
+
reason: 'notToExist() requires ElementQuery',
|
|
162
|
+
details: {},
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Assert that element exists AND is visible (in_viewport=true, is_occluded=false).
|
|
168
|
+
*
|
|
169
|
+
* @returns Predicate function for use with runtime.assert()
|
|
170
|
+
*
|
|
171
|
+
* @example
|
|
172
|
+
* await runtime.assert(
|
|
173
|
+
* expect(E({ textContains: "Checkout" })).toBeVisible(),
|
|
174
|
+
* "checkout_button_visible"
|
|
175
|
+
* );
|
|
176
|
+
*/
|
|
177
|
+
toBeVisible() {
|
|
178
|
+
const query = this._query;
|
|
179
|
+
return (ctx) => {
|
|
180
|
+
const snap = ctx.snapshot;
|
|
181
|
+
if (!snap) {
|
|
182
|
+
return {
|
|
183
|
+
passed: false,
|
|
184
|
+
reason: 'no snapshot available',
|
|
185
|
+
details: { query: queryToDict(query) },
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if (query instanceof query_1.ElementQuery) {
|
|
189
|
+
const matches = query.findAll(snap);
|
|
190
|
+
if (matches.length === 0) {
|
|
191
|
+
return {
|
|
192
|
+
passed: false,
|
|
193
|
+
reason: `no elements matched query: ${JSON.stringify(queryToDict(query))}`,
|
|
194
|
+
details: { query: queryToDict(query), matched: 0 },
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
// Check visibility of first match
|
|
198
|
+
const el = matches[0];
|
|
199
|
+
const isVisible = el.in_viewport && !el.is_occluded;
|
|
200
|
+
return {
|
|
201
|
+
passed: isVisible,
|
|
202
|
+
reason: isVisible
|
|
203
|
+
? ''
|
|
204
|
+
: `element found but not visible (in_viewport=${el.in_viewport}, is_occluded=${el.is_occluded})`,
|
|
205
|
+
details: {
|
|
206
|
+
query: queryToDict(query),
|
|
207
|
+
elementId: el.id,
|
|
208
|
+
inViewport: el.in_viewport,
|
|
209
|
+
isOccluded: el.is_occluded,
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
return {
|
|
214
|
+
passed: false,
|
|
215
|
+
reason: 'toBeVisible() requires ElementQuery',
|
|
216
|
+
details: {},
|
|
217
|
+
};
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Assert that element's text contains the specified substring.
|
|
222
|
+
*
|
|
223
|
+
* @param text - Substring to search for (case-insensitive)
|
|
224
|
+
* @returns Predicate function for use with runtime.assert()
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
* await runtime.assert(
|
|
228
|
+
* expect(inDominantList().nth(0)).toHaveTextContains("Show HN"),
|
|
229
|
+
* "first_item_is_show_hn"
|
|
230
|
+
* );
|
|
231
|
+
*/
|
|
232
|
+
toHaveTextContains(text) {
|
|
233
|
+
const query = this._query;
|
|
234
|
+
return (ctx) => {
|
|
235
|
+
const snap = ctx.snapshot;
|
|
236
|
+
if (!snap) {
|
|
237
|
+
return {
|
|
238
|
+
passed: false,
|
|
239
|
+
reason: 'no snapshot available',
|
|
240
|
+
details: { query: queryToDict(query), expectedText: text },
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
if (query instanceof query_1.ElementQuery) {
|
|
244
|
+
const matches = query.findAll(snap);
|
|
245
|
+
if (matches.length === 0) {
|
|
246
|
+
return {
|
|
247
|
+
passed: false,
|
|
248
|
+
reason: `no elements matched query: ${JSON.stringify(queryToDict(query))}`,
|
|
249
|
+
details: { query: queryToDict(query), matched: 0, expectedText: text },
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
// Check text of first match
|
|
253
|
+
const el = matches[0];
|
|
254
|
+
const elText = el.text || '';
|
|
255
|
+
const ok = elText.toLowerCase().includes(text.toLowerCase());
|
|
256
|
+
return {
|
|
257
|
+
passed: ok,
|
|
258
|
+
reason: ok ? '' : `element text '${elText.substring(0, 100)}' does not contain '${text}'`,
|
|
259
|
+
details: {
|
|
260
|
+
query: queryToDict(query),
|
|
261
|
+
elementId: el.id,
|
|
262
|
+
elementText: elText.substring(0, 200),
|
|
263
|
+
expectedText: text,
|
|
264
|
+
},
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
return {
|
|
268
|
+
passed: false,
|
|
269
|
+
reason: 'toHaveTextContains() requires ElementQuery',
|
|
270
|
+
details: {},
|
|
271
|
+
};
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
exports.ExpectBuilder = ExpectBuilder;
|
|
276
|
+
/**
|
|
277
|
+
* Factory for creating ExpectBuilder instances and global assertions.
|
|
278
|
+
*
|
|
279
|
+
* This is the main entry point for the assertion DSL.
|
|
280
|
+
*
|
|
281
|
+
* @example
|
|
282
|
+
* import { expect, E } from './asserts';
|
|
283
|
+
*
|
|
284
|
+
* // Element-based assertions
|
|
285
|
+
* expect(E({ role: "button" })).toExist()
|
|
286
|
+
* expect(E({ textContains: "Error" })).notToExist()
|
|
287
|
+
*
|
|
288
|
+
* // Global text assertions
|
|
289
|
+
* expect.textPresent("Welcome back")
|
|
290
|
+
* expect.noText("Error")
|
|
291
|
+
*/
|
|
292
|
+
class ExpectFactory {
|
|
293
|
+
/**
|
|
294
|
+
* Create an expectation builder for the given query.
|
|
295
|
+
*
|
|
296
|
+
* @param query - ElementQuery, MultiQuery, or MultiTextPredicate
|
|
297
|
+
* @returns ExpectBuilder for chaining matchers
|
|
298
|
+
*
|
|
299
|
+
* @example
|
|
300
|
+
* expect(E({ role: "button" })).toExist()
|
|
301
|
+
* expect(inDominantList().nth(0)).toHaveTextContains("Show HN")
|
|
302
|
+
*/
|
|
303
|
+
call(query) {
|
|
304
|
+
return new ExpectBuilder(query);
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Global assertion: check if text is present anywhere on the page.
|
|
308
|
+
*
|
|
309
|
+
* Searches across all element text fields.
|
|
310
|
+
*
|
|
311
|
+
* @param text - Text to search for (case-insensitive substring)
|
|
312
|
+
* @returns Predicate function for use with runtime.assert()
|
|
313
|
+
*
|
|
314
|
+
* @example
|
|
315
|
+
* await runtime.assert(
|
|
316
|
+
* expect.textPresent("Welcome back"),
|
|
317
|
+
* "user_logged_in"
|
|
318
|
+
* );
|
|
319
|
+
*/
|
|
320
|
+
textPresent(text) {
|
|
321
|
+
return (ctx) => {
|
|
322
|
+
const snap = ctx.snapshot;
|
|
323
|
+
if (!snap) {
|
|
324
|
+
return {
|
|
325
|
+
passed: false,
|
|
326
|
+
reason: 'no snapshot available',
|
|
327
|
+
details: { searchText: text },
|
|
328
|
+
};
|
|
329
|
+
}
|
|
330
|
+
// Search all element texts
|
|
331
|
+
const textLower = text.toLowerCase();
|
|
332
|
+
for (const el of snap.elements) {
|
|
333
|
+
const elText = el.text || '';
|
|
334
|
+
if (elText.toLowerCase().includes(textLower)) {
|
|
335
|
+
return {
|
|
336
|
+
passed: true,
|
|
337
|
+
reason: '',
|
|
338
|
+
details: { searchText: text, foundInElement: el.id },
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
return {
|
|
343
|
+
passed: false,
|
|
344
|
+
reason: `text '${text}' not found on page`,
|
|
345
|
+
details: { searchText: text, elementsSearched: snap.elements.length },
|
|
346
|
+
};
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
/**
|
|
350
|
+
* Global assertion: check that text is NOT present anywhere on the page.
|
|
351
|
+
*
|
|
352
|
+
* Searches across all element text fields.
|
|
353
|
+
*
|
|
354
|
+
* @param text - Text that should not be present (case-insensitive substring)
|
|
355
|
+
* @returns Predicate function for use with runtime.assert()
|
|
356
|
+
*
|
|
357
|
+
* @example
|
|
358
|
+
* await runtime.assert(
|
|
359
|
+
* expect.noText("Error"),
|
|
360
|
+
* "no_error_message"
|
|
361
|
+
* );
|
|
362
|
+
*/
|
|
363
|
+
noText(text) {
|
|
364
|
+
return (ctx) => {
|
|
365
|
+
const snap = ctx.snapshot;
|
|
366
|
+
if (!snap) {
|
|
367
|
+
return {
|
|
368
|
+
passed: false,
|
|
369
|
+
reason: 'no snapshot available',
|
|
370
|
+
details: { searchText: text },
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
// Search all element texts
|
|
374
|
+
const textLower = text.toLowerCase();
|
|
375
|
+
for (const el of snap.elements) {
|
|
376
|
+
const elText = el.text || '';
|
|
377
|
+
if (elText.toLowerCase().includes(textLower)) {
|
|
378
|
+
return {
|
|
379
|
+
passed: false,
|
|
380
|
+
reason: `text '${text}' found in element id=${el.id}`,
|
|
381
|
+
details: {
|
|
382
|
+
searchText: text,
|
|
383
|
+
foundInElement: el.id,
|
|
384
|
+
elementText: elText.substring(0, 200),
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return {
|
|
390
|
+
passed: true,
|
|
391
|
+
reason: '',
|
|
392
|
+
details: { searchText: text, elementsSearched: snap.elements.length },
|
|
393
|
+
};
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
// Create the singleton factory
|
|
398
|
+
const factoryInstance = new ExpectFactory();
|
|
399
|
+
/**
|
|
400
|
+
* Main entry point for the assertion DSL.
|
|
401
|
+
*
|
|
402
|
+
* Use as a function to create element-based assertions:
|
|
403
|
+
* expect(E({ role: "button" })).toExist()
|
|
404
|
+
*
|
|
405
|
+
* Use static methods for global assertions:
|
|
406
|
+
* expect.textPresent("Welcome")
|
|
407
|
+
* expect.noText("Error")
|
|
408
|
+
*/
|
|
409
|
+
exports.expect = Object.assign((query) => factoryInstance.call(query), {
|
|
410
|
+
textPresent: (text) => factoryInstance.textPresent(text),
|
|
411
|
+
noText: (text) => factoryInstance.noText(text),
|
|
412
|
+
});
|
|
413
|
+
/**
|
|
414
|
+
* Wrapper that adds retry logic to a predicate.
|
|
415
|
+
*
|
|
416
|
+
* Created by withEventually(). Provides an async evaluate() method
|
|
417
|
+
* that retries the predicate with fresh snapshots.
|
|
418
|
+
*
|
|
419
|
+
* Note: TypeScript uses milliseconds for timeout/poll.
|
|
420
|
+
*/
|
|
421
|
+
class EventuallyWrapper {
|
|
422
|
+
constructor(predicate, config = {}) {
|
|
423
|
+
this._predicate = predicate;
|
|
424
|
+
this._config = {
|
|
425
|
+
timeout: config.timeout ?? DEFAULT_TIMEOUT,
|
|
426
|
+
poll: config.poll ?? DEFAULT_POLL,
|
|
427
|
+
maxRetries: config.maxRetries ?? DEFAULT_MAX_RETRIES,
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
/**
|
|
431
|
+
* Evaluate predicate with retry logic.
|
|
432
|
+
*
|
|
433
|
+
* @param ctx - Initial assertion context
|
|
434
|
+
* @param snapshotFn - Async function to take fresh snapshots
|
|
435
|
+
* @returns Promise resolving to AssertOutcome
|
|
436
|
+
*/
|
|
437
|
+
async evaluate(ctx, snapshotFn) {
|
|
438
|
+
const startTime = Date.now();
|
|
439
|
+
let lastOutcome = null;
|
|
440
|
+
let attempts = 0;
|
|
441
|
+
while (true) {
|
|
442
|
+
// Check timeout (higher precedence than maxRetries)
|
|
443
|
+
const elapsed = Date.now() - startTime;
|
|
444
|
+
if (elapsed >= this._config.timeout) {
|
|
445
|
+
if (lastOutcome) {
|
|
446
|
+
lastOutcome.reason = `timeout after ${elapsed}ms: ${lastOutcome.reason}`;
|
|
447
|
+
return lastOutcome;
|
|
448
|
+
}
|
|
449
|
+
return {
|
|
450
|
+
passed: false,
|
|
451
|
+
reason: `timeout after ${elapsed}ms`,
|
|
452
|
+
details: { attempts },
|
|
453
|
+
};
|
|
454
|
+
}
|
|
455
|
+
// Check max retries
|
|
456
|
+
if (attempts >= this._config.maxRetries) {
|
|
457
|
+
if (lastOutcome) {
|
|
458
|
+
lastOutcome.reason = `max retries (${this._config.maxRetries}) exceeded: ${lastOutcome.reason}`;
|
|
459
|
+
return lastOutcome;
|
|
460
|
+
}
|
|
461
|
+
return {
|
|
462
|
+
passed: false,
|
|
463
|
+
reason: `max retries (${this._config.maxRetries}) exceeded`,
|
|
464
|
+
details: { attempts },
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
// Take fresh snapshot if not first attempt
|
|
468
|
+
if (attempts > 0) {
|
|
469
|
+
try {
|
|
470
|
+
const freshSnapshot = await snapshotFn();
|
|
471
|
+
ctx = {
|
|
472
|
+
snapshot: freshSnapshot,
|
|
473
|
+
url: freshSnapshot?.url ?? ctx.url,
|
|
474
|
+
stepId: ctx.stepId,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
catch (e) {
|
|
478
|
+
lastOutcome = {
|
|
479
|
+
passed: false,
|
|
480
|
+
reason: `failed to take snapshot: ${e}`,
|
|
481
|
+
details: { attempts, error: String(e) },
|
|
482
|
+
};
|
|
483
|
+
attempts++;
|
|
484
|
+
await this.sleep(this._config.poll);
|
|
485
|
+
continue;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
// Evaluate predicate
|
|
489
|
+
const outcome = this._predicate(ctx);
|
|
490
|
+
if (outcome.passed) {
|
|
491
|
+
outcome.details.attempts = attempts + 1;
|
|
492
|
+
return outcome;
|
|
493
|
+
}
|
|
494
|
+
lastOutcome = outcome;
|
|
495
|
+
attempts++;
|
|
496
|
+
// Wait before next retry
|
|
497
|
+
if (attempts < this._config.maxRetries) {
|
|
498
|
+
// Check if we'd exceed timeout with the poll delay
|
|
499
|
+
if (Date.now() - startTime + this._config.poll < this._config.timeout) {
|
|
500
|
+
await this.sleep(this._config.poll);
|
|
501
|
+
}
|
|
502
|
+
else {
|
|
503
|
+
// No point waiting, we'll timeout anyway
|
|
504
|
+
lastOutcome.reason = `timeout after ${Date.now() - startTime}ms: ${lastOutcome.reason}`;
|
|
505
|
+
return lastOutcome;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
sleep(ms) {
|
|
511
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
512
|
+
}
|
|
513
|
+
/** Get the configured timeout in milliseconds */
|
|
514
|
+
get timeout() {
|
|
515
|
+
return this._config.timeout;
|
|
516
|
+
}
|
|
517
|
+
/** Get the configured poll interval in milliseconds */
|
|
518
|
+
get poll() {
|
|
519
|
+
return this._config.poll;
|
|
520
|
+
}
|
|
521
|
+
/** Get the configured max retries */
|
|
522
|
+
get maxRetries() {
|
|
523
|
+
return this._config.maxRetries;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
exports.EventuallyWrapper = EventuallyWrapper;
|
|
527
|
+
/**
|
|
528
|
+
* Wrap a predicate with retry logic.
|
|
529
|
+
*
|
|
530
|
+
* This is the TypeScript API for .eventually(). Returns a wrapper
|
|
531
|
+
* that provides an async evaluate() method for use with the runtime.
|
|
532
|
+
*
|
|
533
|
+
* @param predicate - Predicate to wrap
|
|
534
|
+
* @param config - Retry configuration (timeout/poll in milliseconds)
|
|
535
|
+
* @returns EventuallyWrapper with async evaluate() method
|
|
536
|
+
*
|
|
537
|
+
* @example
|
|
538
|
+
* const wrapper = withEventually(
|
|
539
|
+
* expect(E({ role: "button" })).toExist(),
|
|
540
|
+
* { timeout: 5000, maxRetries: 10 }
|
|
541
|
+
* );
|
|
542
|
+
* const result = await wrapper.evaluate(ctx, runtime.snapshot);
|
|
543
|
+
*/
|
|
544
|
+
function withEventually(predicate, config) {
|
|
545
|
+
return new EventuallyWrapper(predicate, config);
|
|
546
|
+
}
|
|
547
|
+
//# sourceMappingURL=expect.js.map
|