@riddledc/riddle-proof 0.7.46 → 0.7.48

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
@@ -229,21 +229,29 @@ mock hits. Add `request_body_contains` / `request_body_patterns` or
229
229
  is part of the contract, such as proving that a save request references the
230
230
  current build ID returned by a prior mocked build response and not a stale one.
231
231
  Body assertions use the full request body for matching and store only length
232
- plus a compact sample in the proof evidence.
232
+ plus a compact sample in the proof evidence. For sequenced mocks, the same
233
+ request-body fields may also be placed on individual `responses[]` entries when
234
+ each step has a different request contract, such as a fail-then-success retry
235
+ where the second request must carry newer state.
233
236
 
234
237
  `target.setup_actions` is optional. Use it when the meaningful proof surface
235
238
  appears only after a picker, tab, login stub, storage seed, form fill,
236
239
  transport control, or other bounded interaction. Supported setup actions are
237
- `click`, `fill`, `set_input_value`, `local_storage`, `session_storage`,
238
- `clear_storage`, `wait`, `wait_for_selector`, and `wait_for_text`; a failed
239
- setup action is recorded as a failed `setup_actions_succeeded` check so the
240
- profile cannot pass without reaching the intended state. Text-matched `click`
241
- actions prefer visible matching elements, which keeps responsive layouts from
242
- selecting hidden desktop or mobile-only links. `local_storage` and
243
- `session_storage` accept a `key` plus string `value` or JSON `json` /
244
- `value_json`, and can reload the page with `reload: true`. `clear_storage`
245
- clears `local`, `session`, or `both` browser storage scopes, defaults to
246
- `both`, and can also reload with `reload: true`.
240
+ `click`, `fill`, `set_input_value`, `assert_text_visible`,
241
+ `assert_text_absent`, `assert_selector_count`, `local_storage`,
242
+ `session_storage`, `clear_storage`, `wait`, `wait_for_selector`, and
243
+ `wait_for_text`; a failed setup action is recorded as a failed
244
+ `setup_actions_succeeded` check so the profile cannot pass without reaching
245
+ the intended state. Text-matched `click` actions prefer visible matching
246
+ elements, which keeps responsive layouts from selecting hidden desktop or
247
+ mobile-only links. Use setup assertions when the pre-click or pre-navigation
248
+ state is part of the contract, for example a fresh row must be present, stale
249
+ copy must be absent, or exactly one source link must exist before clicking into
250
+ the final route. `assert_selector_count` accepts `expected_count`.
251
+ `local_storage` and `session_storage` accept a `key` plus string `value` or
252
+ JSON `json` / `value_json`, and can reload the page with `reload: true`.
253
+ `clear_storage` clears `local`, `session`, or `both` browser storage scopes,
254
+ defaults to `both`, and can also reload with `reload: true`.
247
255
 
248
256
  `target.timeout_sec` is optional. Use it for known-heavy profile targets so the
249
257
  profile carries its own hosted Riddle worker budget; an explicit CLI `--timeout`
@@ -32,6 +32,9 @@ var RIDDLE_PROOF_PROFILE_SETUP_ACTION_TYPES = [
32
32
  "click",
33
33
  "fill",
34
34
  "set_input_value",
35
+ "assert_text_visible",
36
+ "assert_text_absent",
37
+ "assert_selector_count",
35
38
  "local_storage",
36
39
  "session_storage",
37
40
  "clear_storage",
@@ -205,12 +208,19 @@ function normalizeSetupAction(input, index) {
205
208
  if (!isRecord(input)) throw new Error(`target.setup_actions[${index}] must be an object.`);
206
209
  const type = normalizeSetupActionType(stringValue(input.type), index);
207
210
  const selector = stringValue(input.selector);
208
- if ((type === "click" || type === "fill" || type === "set_input_value" || type === "wait_for_selector" || type === "wait_for_text") && !selector) {
211
+ if ((type === "click" || type === "fill" || type === "set_input_value" || type === "wait_for_selector" || type === "wait_for_text" || type === "assert_text_visible" || type === "assert_text_absent" || type === "assert_selector_count") && !selector) {
209
212
  throw new Error(`target.setup_actions[${index}] ${type} requires selector.`);
210
213
  }
211
214
  if (type === "wait_for_text" && !stringValue(input.text) && !stringValue(input.pattern)) {
212
215
  throw new Error(`target.setup_actions[${index}] wait_for_text requires text or pattern.`);
213
216
  }
217
+ if ((type === "assert_text_visible" || type === "assert_text_absent") && !stringValue(input.text) && !stringValue(input.pattern)) {
218
+ throw new Error(`target.setup_actions[${index}] ${type} requires text or pattern.`);
219
+ }
220
+ const expectedCount = numberValue(input.expected_count ?? input.expectedCount ?? input.count);
221
+ if (type === "assert_selector_count" && (expectedCount === void 0 || !Number.isInteger(expectedCount) || expectedCount < 0)) {
222
+ throw new Error(`target.setup_actions[${index}] ${type} requires non-negative integer expected_count.`);
223
+ }
214
224
  const value = stringFromOwn(input, "value", "input_value", "inputValue");
215
225
  const hasJsonValue = hasOwn(input, "value_json") || hasOwn(input, "valueJson") || hasOwn(input, "json");
216
226
  if ((type === "fill" || type === "set_input_value") && value === void 0 && !hasJsonValue) {
@@ -233,6 +243,7 @@ function normalizeSetupAction(input, index) {
233
243
  pattern: stringValue(input.pattern),
234
244
  flags: stringValue(input.flags),
235
245
  index: numberValue(input.index),
246
+ expected_count: expectedCount,
236
247
  ms: numberValue(input.ms) ?? numberValue(input.wait_ms) ?? numberValue(input.waitMs),
237
248
  timeout_ms: numberValue(input.timeout_ms) ?? numberValue(input.timeoutMs),
238
249
  after_ms: numberValue(input.after_ms) ?? numberValue(input.afterMs),
@@ -253,24 +264,7 @@ function normalizeNetworkMock(input, index) {
253
264
  const payload = normalizeNetworkMockResponsePayload(input, `target.network_mocks[${index}]`);
254
265
  const responsesInput = input.responses ?? input.sequence;
255
266
  const responses = normalizeNetworkMockResponses(responsesInput, index, payload);
256
- const requestBodyContains = normalizeStringList(
257
- input.request_body_contains ?? input.requestBodyContains ?? input.body_contains ?? input.bodyContains,
258
- `target.network_mocks[${index}].request_body_contains`
259
- );
260
- const requestBodyPatterns = normalizeStringList(
261
- input.request_body_patterns ?? input.requestBodyPatterns ?? input.body_patterns ?? input.bodyPatterns,
262
- `target.network_mocks[${index}].request_body_patterns`
263
- );
264
- validateRegexPatterns(requestBodyPatterns, `target.network_mocks[${index}].request_body_patterns`);
265
- const requestBodyNotContains = normalizeStringList(
266
- input.request_body_not_contains ?? input.requestBodyNotContains ?? input.request_body_absent ?? input.requestBodyAbsent ?? input.body_not_contains ?? input.bodyNotContains ?? input.body_absent ?? input.bodyAbsent,
267
- `target.network_mocks[${index}].request_body_not_contains`
268
- );
269
- const requestBodyNotPatterns = normalizeStringList(
270
- input.request_body_not_patterns ?? input.requestBodyNotPatterns ?? input.request_body_forbidden_patterns ?? input.requestBodyForbiddenPatterns ?? input.body_not_patterns ?? input.bodyNotPatterns ?? input.body_forbidden_patterns ?? input.bodyForbiddenPatterns,
271
- `target.network_mocks[${index}].request_body_not_patterns`
272
- );
273
- validateRegexPatterns(requestBodyNotPatterns, `target.network_mocks[${index}].request_body_not_patterns`);
267
+ const requestBody = normalizeNetworkMockRequestBodyConstraints(input, `target.network_mocks[${index}]`);
274
268
  const requiredHitCount = numberValue(
275
269
  input.required_hit_count ?? input.requiredHitCount ?? input.required_hits ?? input.requiredHits ?? input.min_hits ?? input.minHits
276
270
  );
@@ -286,6 +280,33 @@ function normalizeNetworkMock(input, index) {
286
280
  repeat_responses: input.repeat_responses === true || input.repeatResponses === true || input.cycle_responses === true || input.cycleResponses === true,
287
281
  required_hit_count: requiredHitCount,
288
282
  required: input.required === false ? false : true,
283
+ capture_request_body: requestBody.capture_request_body,
284
+ request_body_contains: requestBody.request_body_contains,
285
+ request_body_patterns: requestBody.request_body_patterns,
286
+ request_body_not_contains: requestBody.request_body_not_contains,
287
+ request_body_not_patterns: requestBody.request_body_not_patterns
288
+ };
289
+ }
290
+ function normalizeNetworkMockRequestBodyConstraints(input, label) {
291
+ const requestBodyContains = normalizeStringList(
292
+ input.request_body_contains ?? input.requestBodyContains ?? input.body_contains ?? input.bodyContains,
293
+ `${label}.request_body_contains`
294
+ );
295
+ const requestBodyPatterns = normalizeStringList(
296
+ input.request_body_patterns ?? input.requestBodyPatterns ?? input.body_patterns ?? input.bodyPatterns,
297
+ `${label}.request_body_patterns`
298
+ );
299
+ validateRegexPatterns(requestBodyPatterns, `${label}.request_body_patterns`);
300
+ const requestBodyNotContains = normalizeStringList(
301
+ input.request_body_not_contains ?? input.requestBodyNotContains ?? input.request_body_absent ?? input.requestBodyAbsent ?? input.body_not_contains ?? input.bodyNotContains ?? input.body_absent ?? input.bodyAbsent,
302
+ `${label}.request_body_not_contains`
303
+ );
304
+ const requestBodyNotPatterns = normalizeStringList(
305
+ input.request_body_not_patterns ?? input.requestBodyNotPatterns ?? input.request_body_forbidden_patterns ?? input.requestBodyForbiddenPatterns ?? input.body_not_patterns ?? input.bodyNotPatterns ?? input.body_forbidden_patterns ?? input.bodyForbiddenPatterns,
306
+ `${label}.request_body_not_patterns`
307
+ );
308
+ validateRegexPatterns(requestBodyNotPatterns, `${label}.request_body_not_patterns`);
309
+ return {
289
310
  capture_request_body: input.capture_request_body === true || input.captureRequestBody === true || Boolean(requestBodyContains?.length) || Boolean(requestBodyPatterns?.length) || Boolean(requestBodyNotContains?.length) || Boolean(requestBodyNotPatterns?.length),
290
311
  request_body_contains: requestBodyContains,
291
312
  request_body_patterns: requestBodyPatterns,
@@ -300,20 +321,33 @@ function normalizeNetworkMockResponsePayload(input, label, defaults = {}) {
300
321
  }
301
322
  const body = stringValue(input.body) ?? stringValue(input.body_text) ?? stringValue(input.bodyText) ?? defaults.body;
302
323
  const hasJsonBody = Object.prototype.hasOwnProperty.call(input, "body_json") || Object.prototype.hasOwnProperty.call(input, "bodyJson") || Object.prototype.hasOwnProperty.call(input, "json");
324
+ const requestBody = normalizeNetworkMockRequestBodyConstraints(input, label);
303
325
  return {
304
326
  label: stringValue(input.label) || stringValue(input.name) || defaults.label,
305
327
  status,
306
328
  content_type: stringValue(input.content_type) || stringValue(input.contentType) || defaults.content_type,
307
329
  headers: stringRecord(input.headers) || defaults.headers,
308
330
  body,
309
- body_json: hasJsonBody ? toJsonValue(input.body_json ?? input.bodyJson ?? input.json) : defaults.body_json
331
+ body_json: hasJsonBody ? toJsonValue(input.body_json ?? input.bodyJson ?? input.json) : defaults.body_json,
332
+ capture_request_body: requestBody.capture_request_body,
333
+ request_body_contains: requestBody.request_body_contains,
334
+ request_body_patterns: requestBody.request_body_patterns,
335
+ request_body_not_contains: requestBody.request_body_not_contains,
336
+ request_body_not_patterns: requestBody.request_body_not_patterns
310
337
  };
311
338
  }
312
339
  function normalizeNetworkMockResponses(value, mockIndex, defaults) {
313
340
  if (value === void 0) return void 0;
314
341
  if (!Array.isArray(value)) throw new Error(`target.network_mocks[${mockIndex}].responses must be an array.`);
315
342
  if (!value.length) throw new Error(`target.network_mocks[${mockIndex}].responses must not be empty.`);
316
- const responseDefaults = { ...defaults, label: void 0 };
343
+ const responseDefaults = {
344
+ label: void 0,
345
+ status: defaults.status,
346
+ content_type: defaults.content_type,
347
+ headers: defaults.headers,
348
+ body: defaults.body,
349
+ body_json: defaults.body_json
350
+ };
317
351
  return value.map((response, responseIndex) => {
318
352
  if (!isRecord(response)) {
319
353
  throw new Error(`target.network_mocks[${mockIndex}].responses[${responseIndex}] must be an object.`);
@@ -1056,6 +1090,9 @@ function assessNetworkMocksFromEvidence(profile, evidence) {
1056
1090
  if (event.request_body_matches === false) {
1057
1091
  failed.push({
1058
1092
  label: event.label ?? null,
1093
+ response_label: event.response_label ?? null,
1094
+ hit_index: event.hit_index ?? null,
1095
+ response_index: event.response_index ?? null,
1059
1096
  url: event.url ?? null,
1060
1097
  method: event.method ?? null,
1061
1098
  reason: "request_body_mismatch",
@@ -1439,6 +1476,9 @@ function assessProfile(profile, evidence) {
1439
1476
  if (event && event.request_body_matches === false) {
1440
1477
  failed.push({
1441
1478
  label: event.label || null,
1479
+ response_label: event.response_label || null,
1480
+ hit_index: event.hit_index ?? null,
1481
+ response_index: event.response_index ?? null,
1442
1482
  url: event.url || null,
1443
1483
  method: event.method || null,
1444
1484
  reason: "request_body_mismatch",
@@ -1903,20 +1943,21 @@ function compactNetworkMockRequestBody(value) {
1903
1943
  function networkMockStringList(value) {
1904
1944
  return Array.isArray(value) ? value.map((item) => String(item)).filter(Boolean) : [];
1905
1945
  }
1906
- function networkMockShouldCaptureRequestBody(mock) {
1907
- return mock && (
1908
- mock.capture_request_body === true
1909
- || networkMockStringList(mock.request_body_contains).length > 0
1910
- || networkMockStringList(mock.request_body_patterns).length > 0
1911
- || networkMockStringList(mock.request_body_not_contains).length > 0
1912
- || networkMockStringList(mock.request_body_not_patterns).length > 0
1913
- );
1946
+ function networkMockShouldCaptureRequestBody(...sources) {
1947
+ return sources.some((source) => source && (
1948
+ source.capture_request_body === true
1949
+ || networkMockStringList(source.request_body_contains).length > 0
1950
+ || networkMockStringList(source.request_body_patterns).length > 0
1951
+ || networkMockStringList(source.request_body_not_contains).length > 0
1952
+ || networkMockStringList(source.request_body_not_patterns).length > 0
1953
+ ));
1914
1954
  }
1915
- function networkMockRequestBodyFailures(body, mock) {
1955
+ function networkMockRequestBodyFailuresForSource(body, source) {
1916
1956
  const failures = [];
1957
+ if (!source) return failures;
1917
1958
  const rawBody = String(body || "");
1918
1959
  const compactBody = compactNetworkMockRequestBody(rawBody);
1919
- for (const expected of networkMockStringList(mock.request_body_contains)) {
1960
+ for (const expected of networkMockStringList(source.request_body_contains)) {
1920
1961
  if (!rawBody.includes(expected) && !compactBody.includes(expected)) {
1921
1962
  failures.push({
1922
1963
  type: "request_body_missing_text",
@@ -1924,7 +1965,7 @@ function networkMockRequestBodyFailures(body, mock) {
1924
1965
  });
1925
1966
  }
1926
1967
  }
1927
- for (const pattern of networkMockStringList(mock.request_body_patterns)) {
1968
+ for (const pattern of networkMockStringList(source.request_body_patterns)) {
1928
1969
  try {
1929
1970
  const regex = new RegExp(pattern);
1930
1971
  if (!regex.test(rawBody) && !regex.test(compactBody)) {
@@ -1941,7 +1982,7 @@ function networkMockRequestBodyFailures(body, mock) {
1941
1982
  });
1942
1983
  }
1943
1984
  }
1944
- for (const forbidden of networkMockStringList(mock.request_body_not_contains)) {
1985
+ for (const forbidden of networkMockStringList(source.request_body_not_contains)) {
1945
1986
  if (rawBody.includes(forbidden) || compactBody.includes(forbidden)) {
1946
1987
  failures.push({
1947
1988
  type: "request_body_forbidden_text",
@@ -1949,7 +1990,7 @@ function networkMockRequestBodyFailures(body, mock) {
1949
1990
  });
1950
1991
  }
1951
1992
  }
1952
- for (const pattern of networkMockStringList(mock.request_body_not_patterns)) {
1993
+ for (const pattern of networkMockStringList(source.request_body_not_patterns)) {
1953
1994
  try {
1954
1995
  const regex = new RegExp(pattern);
1955
1996
  if (regex.test(rawBody) || regex.test(compactBody)) {
@@ -1968,6 +2009,9 @@ function networkMockRequestBodyFailures(body, mock) {
1968
2009
  }
1969
2010
  return failures;
1970
2011
  }
2012
+ function networkMockRequestBodyFailures(body, ...sources) {
2013
+ return sources.flatMap((source) => networkMockRequestBodyFailuresForSource(body, source));
2014
+ }
1971
2015
  async function setupLocatorVisible(locator, index) {
1972
2016
  return await locator.nth(index).isVisible({ timeout: 1000 }).catch(() => false);
1973
2017
  }
@@ -1996,9 +2040,10 @@ async function registerNetworkMocks(mocks) {
1996
2040
  body = JSON.stringify(response.body_json);
1997
2041
  contentType = response.content_type || headers["content-type"] || headers["Content-Type"] || "application/json";
1998
2042
  }
1999
- const shouldCaptureRequestBody = networkMockShouldCaptureRequestBody(mock);
2043
+ const responseBodyContract = responseIndex === null ? null : response;
2044
+ const shouldCaptureRequestBody = networkMockShouldCaptureRequestBody(mock, responseBodyContract);
2000
2045
  const requestBody = shouldCaptureRequestBody && request.postData ? request.postData() || "" : "";
2001
- const requestBodyFailures = shouldCaptureRequestBody ? networkMockRequestBodyFailures(requestBody, mock) : [];
2046
+ const requestBodyFailures = shouldCaptureRequestBody ? networkMockRequestBodyFailures(requestBody, mock, responseBodyContract) : [];
2002
2047
  const status = response.status || mock.status || 200;
2003
2048
  const event = {
2004
2049
  ok: true,
@@ -2116,6 +2161,19 @@ async function executeSetupAction(action, ordinal) {
2116
2161
  await locator.nth(targetIndex).fill(value, { timeout });
2117
2162
  return { ...base, ok: true, count, target_index: targetIndex, value_length: value.length };
2118
2163
  }
2164
+ if (type === "assert_selector_count") {
2165
+ const locator = page.locator(action.selector);
2166
+ const expectedCount = setupNumber(action.expected_count, -1);
2167
+ if (!Number.isInteger(expectedCount) || expectedCount < 0) return { ...base, reason: "invalid_expected_count", expected_count: action.expected_count };
2168
+ const startedAt = Date.now();
2169
+ let count = 0;
2170
+ while (Date.now() - startedAt <= timeout) {
2171
+ count = await locator.count().catch(() => 0);
2172
+ if (count === expectedCount) return { ...base, ok: true, count, expected_count: expectedCount, timeout_ms: timeout };
2173
+ await page.waitForTimeout(100);
2174
+ }
2175
+ return { ...base, reason: "selector_count_mismatch", count, expected_count: expectedCount, timeout_ms: timeout };
2176
+ }
2119
2177
  if (type === "wait_for_text") {
2120
2178
  const locator = page.locator(action.selector);
2121
2179
  const startedAt = Date.now();
@@ -2133,6 +2191,47 @@ async function executeSetupAction(action, ordinal) {
2133
2191
  }
2134
2192
  return { ...base, reason: "text_not_found", text: compactSetupResultText(lastText), timeout_ms: timeout };
2135
2193
  }
2194
+ if (type === "assert_text_visible" || type === "assert_text_absent") {
2195
+ const locator = page.locator(action.selector);
2196
+ const startedAt = Date.now();
2197
+ let lastText = "";
2198
+ let matchedText = "";
2199
+ let hiddenMatch = false;
2200
+ while (Date.now() - startedAt <= timeout) {
2201
+ const count = await locator.count().catch(() => 0);
2202
+ let matched = false;
2203
+ matchedText = "";
2204
+ hiddenMatch = false;
2205
+ for (let index = 0; index < count; index += 1) {
2206
+ const text = await setupLocatorText(locator, index);
2207
+ lastText = text || lastText;
2208
+ if (setupTextMatches(text, action)) {
2209
+ matched = true;
2210
+ matchedText = text;
2211
+ if (type === "assert_text_visible") {
2212
+ const visible = await setupLocatorVisible(locator, index);
2213
+ if (visible) {
2214
+ return { ...base, ok: true, count, text: compactSetupResultText(text), target_index: index, timeout_ms: timeout };
2215
+ }
2216
+ hiddenMatch = true;
2217
+ break;
2218
+ }
2219
+ break;
2220
+ }
2221
+ }
2222
+ if (type === "assert_text_absent" && !matched) {
2223
+ return { ...base, ok: true, count, timeout_ms: timeout };
2224
+ }
2225
+ await page.waitForTimeout(100);
2226
+ }
2227
+ if (type === "assert_text_visible") {
2228
+ if (hiddenMatch) {
2229
+ return { ...base, reason: "matching_element_not_visible", text: compactSetupResultText(matchedText), timeout_ms: timeout };
2230
+ }
2231
+ return { ...base, reason: "text_not_found", text: compactSetupResultText(lastText), timeout_ms: timeout };
2232
+ }
2233
+ return { ...base, reason: "text_still_present", text: compactSetupResultText(matchedText || lastText), timeout_ms: timeout };
2234
+ }
2136
2235
  return { ...base, reason: "unsupported_action" };
2137
2236
  } catch (error) {
2138
2237
  return { ...base, error: String(error && error.message ? error.message : error).slice(0, 1000) };
package/dist/cli.cjs CHANGED
@@ -6905,6 +6905,9 @@ var RIDDLE_PROOF_PROFILE_SETUP_ACTION_TYPES = [
6905
6905
  "click",
6906
6906
  "fill",
6907
6907
  "set_input_value",
6908
+ "assert_text_visible",
6909
+ "assert_text_absent",
6910
+ "assert_selector_count",
6908
6911
  "local_storage",
6909
6912
  "session_storage",
6910
6913
  "clear_storage",
@@ -7078,12 +7081,19 @@ function normalizeSetupAction(input, index) {
7078
7081
  if (!isRecord(input)) throw new Error(`target.setup_actions[${index}] must be an object.`);
7079
7082
  const type = normalizeSetupActionType(stringValue2(input.type), index);
7080
7083
  const selector = stringValue2(input.selector);
7081
- if ((type === "click" || type === "fill" || type === "set_input_value" || type === "wait_for_selector" || type === "wait_for_text") && !selector) {
7084
+ if ((type === "click" || type === "fill" || type === "set_input_value" || type === "wait_for_selector" || type === "wait_for_text" || type === "assert_text_visible" || type === "assert_text_absent" || type === "assert_selector_count") && !selector) {
7082
7085
  throw new Error(`target.setup_actions[${index}] ${type} requires selector.`);
7083
7086
  }
7084
7087
  if (type === "wait_for_text" && !stringValue2(input.text) && !stringValue2(input.pattern)) {
7085
7088
  throw new Error(`target.setup_actions[${index}] wait_for_text requires text or pattern.`);
7086
7089
  }
7090
+ if ((type === "assert_text_visible" || type === "assert_text_absent") && !stringValue2(input.text) && !stringValue2(input.pattern)) {
7091
+ throw new Error(`target.setup_actions[${index}] ${type} requires text or pattern.`);
7092
+ }
7093
+ const expectedCount = numberValue(input.expected_count ?? input.expectedCount ?? input.count);
7094
+ if (type === "assert_selector_count" && (expectedCount === void 0 || !Number.isInteger(expectedCount) || expectedCount < 0)) {
7095
+ throw new Error(`target.setup_actions[${index}] ${type} requires non-negative integer expected_count.`);
7096
+ }
7087
7097
  const value = stringFromOwn(input, "value", "input_value", "inputValue");
7088
7098
  const hasJsonValue = hasOwn(input, "value_json") || hasOwn(input, "valueJson") || hasOwn(input, "json");
7089
7099
  if ((type === "fill" || type === "set_input_value") && value === void 0 && !hasJsonValue) {
@@ -7106,6 +7116,7 @@ function normalizeSetupAction(input, index) {
7106
7116
  pattern: stringValue2(input.pattern),
7107
7117
  flags: stringValue2(input.flags),
7108
7118
  index: numberValue(input.index),
7119
+ expected_count: expectedCount,
7109
7120
  ms: numberValue(input.ms) ?? numberValue(input.wait_ms) ?? numberValue(input.waitMs),
7110
7121
  timeout_ms: numberValue(input.timeout_ms) ?? numberValue(input.timeoutMs),
7111
7122
  after_ms: numberValue(input.after_ms) ?? numberValue(input.afterMs),
@@ -7126,24 +7137,7 @@ function normalizeNetworkMock(input, index) {
7126
7137
  const payload = normalizeNetworkMockResponsePayload(input, `target.network_mocks[${index}]`);
7127
7138
  const responsesInput = input.responses ?? input.sequence;
7128
7139
  const responses = normalizeNetworkMockResponses(responsesInput, index, payload);
7129
- const requestBodyContains = normalizeStringList(
7130
- input.request_body_contains ?? input.requestBodyContains ?? input.body_contains ?? input.bodyContains,
7131
- `target.network_mocks[${index}].request_body_contains`
7132
- );
7133
- const requestBodyPatterns = normalizeStringList(
7134
- input.request_body_patterns ?? input.requestBodyPatterns ?? input.body_patterns ?? input.bodyPatterns,
7135
- `target.network_mocks[${index}].request_body_patterns`
7136
- );
7137
- validateRegexPatterns(requestBodyPatterns, `target.network_mocks[${index}].request_body_patterns`);
7138
- const requestBodyNotContains = normalizeStringList(
7139
- input.request_body_not_contains ?? input.requestBodyNotContains ?? input.request_body_absent ?? input.requestBodyAbsent ?? input.body_not_contains ?? input.bodyNotContains ?? input.body_absent ?? input.bodyAbsent,
7140
- `target.network_mocks[${index}].request_body_not_contains`
7141
- );
7142
- const requestBodyNotPatterns = normalizeStringList(
7143
- input.request_body_not_patterns ?? input.requestBodyNotPatterns ?? input.request_body_forbidden_patterns ?? input.requestBodyForbiddenPatterns ?? input.body_not_patterns ?? input.bodyNotPatterns ?? input.body_forbidden_patterns ?? input.bodyForbiddenPatterns,
7144
- `target.network_mocks[${index}].request_body_not_patterns`
7145
- );
7146
- validateRegexPatterns(requestBodyNotPatterns, `target.network_mocks[${index}].request_body_not_patterns`);
7140
+ const requestBody = normalizeNetworkMockRequestBodyConstraints(input, `target.network_mocks[${index}]`);
7147
7141
  const requiredHitCount = numberValue(
7148
7142
  input.required_hit_count ?? input.requiredHitCount ?? input.required_hits ?? input.requiredHits ?? input.min_hits ?? input.minHits
7149
7143
  );
@@ -7159,6 +7153,33 @@ function normalizeNetworkMock(input, index) {
7159
7153
  repeat_responses: input.repeat_responses === true || input.repeatResponses === true || input.cycle_responses === true || input.cycleResponses === true,
7160
7154
  required_hit_count: requiredHitCount,
7161
7155
  required: input.required === false ? false : true,
7156
+ capture_request_body: requestBody.capture_request_body,
7157
+ request_body_contains: requestBody.request_body_contains,
7158
+ request_body_patterns: requestBody.request_body_patterns,
7159
+ request_body_not_contains: requestBody.request_body_not_contains,
7160
+ request_body_not_patterns: requestBody.request_body_not_patterns
7161
+ };
7162
+ }
7163
+ function normalizeNetworkMockRequestBodyConstraints(input, label) {
7164
+ const requestBodyContains = normalizeStringList(
7165
+ input.request_body_contains ?? input.requestBodyContains ?? input.body_contains ?? input.bodyContains,
7166
+ `${label}.request_body_contains`
7167
+ );
7168
+ const requestBodyPatterns = normalizeStringList(
7169
+ input.request_body_patterns ?? input.requestBodyPatterns ?? input.body_patterns ?? input.bodyPatterns,
7170
+ `${label}.request_body_patterns`
7171
+ );
7172
+ validateRegexPatterns(requestBodyPatterns, `${label}.request_body_patterns`);
7173
+ const requestBodyNotContains = normalizeStringList(
7174
+ input.request_body_not_contains ?? input.requestBodyNotContains ?? input.request_body_absent ?? input.requestBodyAbsent ?? input.body_not_contains ?? input.bodyNotContains ?? input.body_absent ?? input.bodyAbsent,
7175
+ `${label}.request_body_not_contains`
7176
+ );
7177
+ const requestBodyNotPatterns = normalizeStringList(
7178
+ input.request_body_not_patterns ?? input.requestBodyNotPatterns ?? input.request_body_forbidden_patterns ?? input.requestBodyForbiddenPatterns ?? input.body_not_patterns ?? input.bodyNotPatterns ?? input.body_forbidden_patterns ?? input.bodyForbiddenPatterns,
7179
+ `${label}.request_body_not_patterns`
7180
+ );
7181
+ validateRegexPatterns(requestBodyNotPatterns, `${label}.request_body_not_patterns`);
7182
+ return {
7162
7183
  capture_request_body: input.capture_request_body === true || input.captureRequestBody === true || Boolean(requestBodyContains?.length) || Boolean(requestBodyPatterns?.length) || Boolean(requestBodyNotContains?.length) || Boolean(requestBodyNotPatterns?.length),
7163
7184
  request_body_contains: requestBodyContains,
7164
7185
  request_body_patterns: requestBodyPatterns,
@@ -7173,20 +7194,33 @@ function normalizeNetworkMockResponsePayload(input, label, defaults = {}) {
7173
7194
  }
7174
7195
  const body = stringValue2(input.body) ?? stringValue2(input.body_text) ?? stringValue2(input.bodyText) ?? defaults.body;
7175
7196
  const hasJsonBody = Object.prototype.hasOwnProperty.call(input, "body_json") || Object.prototype.hasOwnProperty.call(input, "bodyJson") || Object.prototype.hasOwnProperty.call(input, "json");
7197
+ const requestBody = normalizeNetworkMockRequestBodyConstraints(input, label);
7176
7198
  return {
7177
7199
  label: stringValue2(input.label) || stringValue2(input.name) || defaults.label,
7178
7200
  status,
7179
7201
  content_type: stringValue2(input.content_type) || stringValue2(input.contentType) || defaults.content_type,
7180
7202
  headers: stringRecord(input.headers) || defaults.headers,
7181
7203
  body,
7182
- body_json: hasJsonBody ? toJsonValue(input.body_json ?? input.bodyJson ?? input.json) : defaults.body_json
7204
+ body_json: hasJsonBody ? toJsonValue(input.body_json ?? input.bodyJson ?? input.json) : defaults.body_json,
7205
+ capture_request_body: requestBody.capture_request_body,
7206
+ request_body_contains: requestBody.request_body_contains,
7207
+ request_body_patterns: requestBody.request_body_patterns,
7208
+ request_body_not_contains: requestBody.request_body_not_contains,
7209
+ request_body_not_patterns: requestBody.request_body_not_patterns
7183
7210
  };
7184
7211
  }
7185
7212
  function normalizeNetworkMockResponses(value, mockIndex, defaults) {
7186
7213
  if (value === void 0) return void 0;
7187
7214
  if (!Array.isArray(value)) throw new Error(`target.network_mocks[${mockIndex}].responses must be an array.`);
7188
7215
  if (!value.length) throw new Error(`target.network_mocks[${mockIndex}].responses must not be empty.`);
7189
- const responseDefaults = { ...defaults, label: void 0 };
7216
+ const responseDefaults = {
7217
+ label: void 0,
7218
+ status: defaults.status,
7219
+ content_type: defaults.content_type,
7220
+ headers: defaults.headers,
7221
+ body: defaults.body,
7222
+ body_json: defaults.body_json
7223
+ };
7190
7224
  return value.map((response, responseIndex) => {
7191
7225
  if (!isRecord(response)) {
7192
7226
  throw new Error(`target.network_mocks[${mockIndex}].responses[${responseIndex}] must be an object.`);
@@ -7929,6 +7963,9 @@ function assessNetworkMocksFromEvidence(profile, evidence) {
7929
7963
  if (event.request_body_matches === false) {
7930
7964
  failed.push({
7931
7965
  label: event.label ?? null,
7966
+ response_label: event.response_label ?? null,
7967
+ hit_index: event.hit_index ?? null,
7968
+ response_index: event.response_index ?? null,
7932
7969
  url: event.url ?? null,
7933
7970
  method: event.method ?? null,
7934
7971
  reason: "request_body_mismatch",
@@ -8296,6 +8333,9 @@ function assessProfile(profile, evidence) {
8296
8333
  if (event && event.request_body_matches === false) {
8297
8334
  failed.push({
8298
8335
  label: event.label || null,
8336
+ response_label: event.response_label || null,
8337
+ hit_index: event.hit_index ?? null,
8338
+ response_index: event.response_index ?? null,
8299
8339
  url: event.url || null,
8300
8340
  method: event.method || null,
8301
8341
  reason: "request_body_mismatch",
@@ -8760,20 +8800,21 @@ function compactNetworkMockRequestBody(value) {
8760
8800
  function networkMockStringList(value) {
8761
8801
  return Array.isArray(value) ? value.map((item) => String(item)).filter(Boolean) : [];
8762
8802
  }
8763
- function networkMockShouldCaptureRequestBody(mock) {
8764
- return mock && (
8765
- mock.capture_request_body === true
8766
- || networkMockStringList(mock.request_body_contains).length > 0
8767
- || networkMockStringList(mock.request_body_patterns).length > 0
8768
- || networkMockStringList(mock.request_body_not_contains).length > 0
8769
- || networkMockStringList(mock.request_body_not_patterns).length > 0
8770
- );
8803
+ function networkMockShouldCaptureRequestBody(...sources) {
8804
+ return sources.some((source) => source && (
8805
+ source.capture_request_body === true
8806
+ || networkMockStringList(source.request_body_contains).length > 0
8807
+ || networkMockStringList(source.request_body_patterns).length > 0
8808
+ || networkMockStringList(source.request_body_not_contains).length > 0
8809
+ || networkMockStringList(source.request_body_not_patterns).length > 0
8810
+ ));
8771
8811
  }
8772
- function networkMockRequestBodyFailures(body, mock) {
8812
+ function networkMockRequestBodyFailuresForSource(body, source) {
8773
8813
  const failures = [];
8814
+ if (!source) return failures;
8774
8815
  const rawBody = String(body || "");
8775
8816
  const compactBody = compactNetworkMockRequestBody(rawBody);
8776
- for (const expected of networkMockStringList(mock.request_body_contains)) {
8817
+ for (const expected of networkMockStringList(source.request_body_contains)) {
8777
8818
  if (!rawBody.includes(expected) && !compactBody.includes(expected)) {
8778
8819
  failures.push({
8779
8820
  type: "request_body_missing_text",
@@ -8781,7 +8822,7 @@ function networkMockRequestBodyFailures(body, mock) {
8781
8822
  });
8782
8823
  }
8783
8824
  }
8784
- for (const pattern of networkMockStringList(mock.request_body_patterns)) {
8825
+ for (const pattern of networkMockStringList(source.request_body_patterns)) {
8785
8826
  try {
8786
8827
  const regex = new RegExp(pattern);
8787
8828
  if (!regex.test(rawBody) && !regex.test(compactBody)) {
@@ -8798,7 +8839,7 @@ function networkMockRequestBodyFailures(body, mock) {
8798
8839
  });
8799
8840
  }
8800
8841
  }
8801
- for (const forbidden of networkMockStringList(mock.request_body_not_contains)) {
8842
+ for (const forbidden of networkMockStringList(source.request_body_not_contains)) {
8802
8843
  if (rawBody.includes(forbidden) || compactBody.includes(forbidden)) {
8803
8844
  failures.push({
8804
8845
  type: "request_body_forbidden_text",
@@ -8806,7 +8847,7 @@ function networkMockRequestBodyFailures(body, mock) {
8806
8847
  });
8807
8848
  }
8808
8849
  }
8809
- for (const pattern of networkMockStringList(mock.request_body_not_patterns)) {
8850
+ for (const pattern of networkMockStringList(source.request_body_not_patterns)) {
8810
8851
  try {
8811
8852
  const regex = new RegExp(pattern);
8812
8853
  if (regex.test(rawBody) || regex.test(compactBody)) {
@@ -8825,6 +8866,9 @@ function networkMockRequestBodyFailures(body, mock) {
8825
8866
  }
8826
8867
  return failures;
8827
8868
  }
8869
+ function networkMockRequestBodyFailures(body, ...sources) {
8870
+ return sources.flatMap((source) => networkMockRequestBodyFailuresForSource(body, source));
8871
+ }
8828
8872
  async function setupLocatorVisible(locator, index) {
8829
8873
  return await locator.nth(index).isVisible({ timeout: 1000 }).catch(() => false);
8830
8874
  }
@@ -8853,9 +8897,10 @@ async function registerNetworkMocks(mocks) {
8853
8897
  body = JSON.stringify(response.body_json);
8854
8898
  contentType = response.content_type || headers["content-type"] || headers["Content-Type"] || "application/json";
8855
8899
  }
8856
- const shouldCaptureRequestBody = networkMockShouldCaptureRequestBody(mock);
8900
+ const responseBodyContract = responseIndex === null ? null : response;
8901
+ const shouldCaptureRequestBody = networkMockShouldCaptureRequestBody(mock, responseBodyContract);
8857
8902
  const requestBody = shouldCaptureRequestBody && request.postData ? request.postData() || "" : "";
8858
- const requestBodyFailures = shouldCaptureRequestBody ? networkMockRequestBodyFailures(requestBody, mock) : [];
8903
+ const requestBodyFailures = shouldCaptureRequestBody ? networkMockRequestBodyFailures(requestBody, mock, responseBodyContract) : [];
8859
8904
  const status = response.status || mock.status || 200;
8860
8905
  const event = {
8861
8906
  ok: true,
@@ -8973,6 +9018,19 @@ async function executeSetupAction(action, ordinal) {
8973
9018
  await locator.nth(targetIndex).fill(value, { timeout });
8974
9019
  return { ...base, ok: true, count, target_index: targetIndex, value_length: value.length };
8975
9020
  }
9021
+ if (type === "assert_selector_count") {
9022
+ const locator = page.locator(action.selector);
9023
+ const expectedCount = setupNumber(action.expected_count, -1);
9024
+ if (!Number.isInteger(expectedCount) || expectedCount < 0) return { ...base, reason: "invalid_expected_count", expected_count: action.expected_count };
9025
+ const startedAt = Date.now();
9026
+ let count = 0;
9027
+ while (Date.now() - startedAt <= timeout) {
9028
+ count = await locator.count().catch(() => 0);
9029
+ if (count === expectedCount) return { ...base, ok: true, count, expected_count: expectedCount, timeout_ms: timeout };
9030
+ await page.waitForTimeout(100);
9031
+ }
9032
+ return { ...base, reason: "selector_count_mismatch", count, expected_count: expectedCount, timeout_ms: timeout };
9033
+ }
8976
9034
  if (type === "wait_for_text") {
8977
9035
  const locator = page.locator(action.selector);
8978
9036
  const startedAt = Date.now();
@@ -8990,6 +9048,47 @@ async function executeSetupAction(action, ordinal) {
8990
9048
  }
8991
9049
  return { ...base, reason: "text_not_found", text: compactSetupResultText(lastText), timeout_ms: timeout };
8992
9050
  }
9051
+ if (type === "assert_text_visible" || type === "assert_text_absent") {
9052
+ const locator = page.locator(action.selector);
9053
+ const startedAt = Date.now();
9054
+ let lastText = "";
9055
+ let matchedText = "";
9056
+ let hiddenMatch = false;
9057
+ while (Date.now() - startedAt <= timeout) {
9058
+ const count = await locator.count().catch(() => 0);
9059
+ let matched = false;
9060
+ matchedText = "";
9061
+ hiddenMatch = false;
9062
+ for (let index = 0; index < count; index += 1) {
9063
+ const text = await setupLocatorText(locator, index);
9064
+ lastText = text || lastText;
9065
+ if (setupTextMatches(text, action)) {
9066
+ matched = true;
9067
+ matchedText = text;
9068
+ if (type === "assert_text_visible") {
9069
+ const visible = await setupLocatorVisible(locator, index);
9070
+ if (visible) {
9071
+ return { ...base, ok: true, count, text: compactSetupResultText(text), target_index: index, timeout_ms: timeout };
9072
+ }
9073
+ hiddenMatch = true;
9074
+ break;
9075
+ }
9076
+ break;
9077
+ }
9078
+ }
9079
+ if (type === "assert_text_absent" && !matched) {
9080
+ return { ...base, ok: true, count, timeout_ms: timeout };
9081
+ }
9082
+ await page.waitForTimeout(100);
9083
+ }
9084
+ if (type === "assert_text_visible") {
9085
+ if (hiddenMatch) {
9086
+ return { ...base, reason: "matching_element_not_visible", text: compactSetupResultText(matchedText), timeout_ms: timeout };
9087
+ }
9088
+ return { ...base, reason: "text_not_found", text: compactSetupResultText(lastText), timeout_ms: timeout };
9089
+ }
9090
+ return { ...base, reason: "text_still_present", text: compactSetupResultText(matchedText || lastText), timeout_ms: timeout };
9091
+ }
8993
9092
  return { ...base, reason: "unsupported_action" };
8994
9093
  } catch (error) {
8995
9094
  return { ...base, error: String(error && error.message ? error.message : error).slice(0, 1000) };
package/dist/cli.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  profileStatusExitCode,
11
11
  resolveRiddleProofProfileTargetUrl,
12
12
  resolveRiddleProofProfileTimeoutSec
13
- } from "./chunk-WSZ5AVKX.js";
13
+ } from "./chunk-V2KNQTQD.js";
14
14
  import {
15
15
  createRiddleApiClient,
16
16
  parseRiddleViewport