explorbot 0.5.8 → 0.5.10

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.
Files changed (83) hide show
  1. package/README.md +1 -1
  2. package/bin/explorbot-cli.ts +25 -4
  3. package/boat/api-tester/src/commands/init-command.ts +2 -0
  4. package/boat/doc-collector/bin/doc-collector-cli.ts +2 -1
  5. package/boat/prima/README.md +3 -0
  6. package/boat/prima/bin/prima-cli.ts +2 -1
  7. package/boat/prima/src/cli.ts +7 -1
  8. package/boat/prima/src/envelope.ts +18 -4
  9. package/boat/prima/src/help.ts +15 -4
  10. package/boat/prima/src/prima.ts +181 -25
  11. package/dist/bin/explorbot-cli.js +31 -4
  12. package/dist/boat/api-tester/bin/apibot-cli.js +2 -1
  13. package/dist/boat/api-tester/src/commands/init-command.js +2 -0
  14. package/dist/boat/doc-collector/bin/doc-collector-cli.js +2 -1
  15. package/dist/boat/prima/bin/prima-cli.js +2 -1
  16. package/dist/boat/prima/src/cli.js +6 -1
  17. package/dist/boat/prima/src/envelope.js +16 -3
  18. package/dist/boat/prima/src/help.js +14 -4
  19. package/dist/boat/prima/src/prima.js +191 -27
  20. package/dist/package.json +1 -1
  21. package/dist/rules/navigator/verification-actions.md +5 -3
  22. package/dist/rules/planner/ignored-pages.md +6 -0
  23. package/dist/rules/quartermaster/ux/accessibility.md +8 -0
  24. package/dist/rules/quartermaster/ux/charts.md +6 -0
  25. package/dist/rules/quartermaster/ux/copy.md +9 -0
  26. package/dist/rules/quartermaster/ux/forms.md +7 -0
  27. package/dist/rules/quartermaster/ux/interaction.md +7 -0
  28. package/dist/rules/quartermaster/ux/motion.md +5 -0
  29. package/dist/rules/quartermaster/ux/navigation.md +7 -0
  30. package/dist/rules/quartermaster/ux/readability.md +6 -0
  31. package/dist/rules/quartermaster/ux/responsive.md +7 -0
  32. package/dist/src/ai/judge.d.ts +3 -1
  33. package/dist/src/ai/judge.js +13 -3
  34. package/dist/src/ai/navigator.js +3 -2
  35. package/dist/src/ai/pilot.js +10 -4
  36. package/dist/src/ai/planner.js +9 -1
  37. package/dist/src/ai/provider.d.ts +4 -0
  38. package/dist/src/ai/provider.js +27 -5
  39. package/dist/src/ai/quartermaster.d.ts +31 -3
  40. package/dist/src/ai/quartermaster.js +262 -17
  41. package/dist/src/ai/tester.js +6 -3
  42. package/dist/src/commands/init-command.js +4 -0
  43. package/dist/src/commands/options/auth-option.d.ts +6 -0
  44. package/dist/src/commands/options/auth-option.js +14 -0
  45. package/dist/src/commands/options/index.d.ts +2 -0
  46. package/dist/src/commands/options/index.js +2 -0
  47. package/dist/src/config.js +1 -0
  48. package/dist/src/explorbot.d.ts +1 -1
  49. package/dist/src/explorbot.js +8 -13
  50. package/dist/src/explorer.js +1 -0
  51. package/dist/src/global-config.js +6 -2
  52. package/dist/src/utils/ux-probe.d.ts +25 -0
  53. package/dist/src/utils/ux-probe.js +155 -0
  54. package/docs/reference/commands.md +12 -0
  55. package/docs/workflow/agentic-usage.md +7 -0
  56. package/docs/workflow/knowledge.md +6 -0
  57. package/package.json +1 -1
  58. package/rules/navigator/verification-actions.md +5 -3
  59. package/rules/planner/ignored-pages.md +6 -0
  60. package/rules/quartermaster/ux/accessibility.md +8 -0
  61. package/rules/quartermaster/ux/charts.md +6 -0
  62. package/rules/quartermaster/ux/copy.md +9 -0
  63. package/rules/quartermaster/ux/forms.md +7 -0
  64. package/rules/quartermaster/ux/interaction.md +7 -0
  65. package/rules/quartermaster/ux/motion.md +5 -0
  66. package/rules/quartermaster/ux/navigation.md +7 -0
  67. package/rules/quartermaster/ux/readability.md +6 -0
  68. package/rules/quartermaster/ux/responsive.md +7 -0
  69. package/src/ai/judge.ts +11 -3
  70. package/src/ai/navigator.ts +2 -2
  71. package/src/ai/pilot.ts +10 -4
  72. package/src/ai/planner.ts +10 -1
  73. package/src/ai/provider.ts +29 -6
  74. package/src/ai/quartermaster.ts +300 -18
  75. package/src/ai/tester.ts +5 -3
  76. package/src/commands/init-command.ts +5 -0
  77. package/src/commands/options/auth-option.ts +15 -0
  78. package/src/commands/options/index.ts +2 -0
  79. package/src/config.ts +1 -0
  80. package/src/explorbot.ts +9 -13
  81. package/src/explorer.ts +1 -0
  82. package/src/global-config.ts +5 -2
  83. package/src/utils/ux-probe.ts +163 -0
@@ -0,0 +1,5 @@
1
+ <motion priority="MEDIUM">
2
+ Judge only what measurements report:
3
+ - Transitions on layout properties, which make content jump while they run.
4
+ - Animations that keep running when the user asks for reduced motion.
5
+ </motion>
@@ -0,0 +1,7 @@
1
+ <navigation priority="HIGH">
2
+ - The current location is marked in the navigation that leads to it.
3
+ - Back behaves predictably; a state worth sharing or returning to has its own URL.
4
+ - Navigation offers few top-level choices; grouping beats a long flat list.
5
+ - Destructive or session-ending items are set apart from ordinary destinations.
6
+ - Modals do not carry primary navigation flows.
7
+ </navigation>
@@ -0,0 +1,6 @@
1
+ <readability priority="MEDIUM">
2
+ Judge only what measurements report:
3
+ - Body text below the minimum size.
4
+ - Multi-line text with cramped line-height.
5
+ - Truncated text that hides meaning and offers no way to read it in full.
6
+ </readability>
@@ -0,0 +1,7 @@
1
+ <responsive priority="HIGH">
2
+ - Compare breakpoints: the focus area must stay usable at every width it was measured at.
3
+ - No horizontal scroll; content fits the viewport width. Zoom must never be disabled.
4
+ - When navigation does not fit, it collapses behind one named, reachable control; nothing primary disappears without a way back to it.
5
+ - On narrow screens core content comes first; secondary panels fold, they do not push the task below the fold.
6
+ - Fixed or sticky bars never cover the content or controls beneath them.
7
+ </responsive>
@@ -5,7 +5,8 @@ export declare const JUDGE_PAGE_CAP = 12000;
5
5
  export declare class Decision {
6
6
  readonly value: string | null;
7
7
  readonly confidence: number;
8
- constructor(value: string | null, confidence: number);
8
+ readonly answered: boolean;
9
+ constructor(value: string | null, confidence: number, answered?: boolean);
9
10
  get approved(): boolean;
10
11
  get rejected(): boolean;
11
12
  }
@@ -23,6 +24,7 @@ export declare class Judge {
23
24
  static fromConfig(config: AIConfig['decisionModel']): Judge | null;
24
25
  get toolEnabled(): boolean;
25
26
  decide(question: string, options: string[] | boolean | null, state: unknown): Promise<Decision>;
27
+ assert(statement: string, state: unknown): Promise<boolean | null>;
26
28
  pick(question: string, options: string[], state: unknown): Promise<number | null>;
27
29
  consult(question: string, options: string[] | boolean | null, state: unknown): Promise<Decision>;
28
30
  request(question: string, options: string[] | boolean | null, state: unknown): Promise<Decision>;
@@ -10,9 +10,11 @@ export const JUDGE_PAGE_CAP = 12000;
10
10
  export class Decision {
11
11
  value;
12
12
  confidence;
13
- constructor(value, confidence) {
13
+ answered;
14
+ constructor(value, confidence, answered = true) {
14
15
  this.value = value;
15
16
  this.confidence = confidence;
17
+ this.answered = answered;
16
18
  }
17
19
  get approved() {
18
20
  return this.value !== null;
@@ -43,9 +45,17 @@ export class Judge {
43
45
  }
44
46
  async decide(question, options, state) {
45
47
  if (!this.enabled.direct)
46
- return new Decision(null, 0);
48
+ return new Decision(null, 0, false);
47
49
  return this.consult(question, options, state);
48
50
  }
51
+ async assert(statement, state) {
52
+ const decision = await this.decide(statement, null, state);
53
+ if (decision.approved)
54
+ return true;
55
+ if (decision.answered && decision.confidence < 1 - this.threshold)
56
+ return false;
57
+ return null;
58
+ }
49
59
  async pick(question, options, state) {
50
60
  if (new Set(options).size < options.length)
51
61
  return null;
@@ -75,7 +85,7 @@ export class Judge {
75
85
  instructions = `${question} ${UNDECIDED_HINT}`;
76
86
  const answer = await this.provider.decide(state, instructions, list).catch((error) => this.recordFailure(error));
77
87
  if (!answer)
78
- return new Decision(null, 0);
88
+ return new Decision(null, 0, false);
79
89
  Observability.getSpan()?.setAttribute('langfuse.observation.output', JSON.stringify(answer));
80
90
  if (answer.probability <= this.threshold)
81
91
  return new Decision(null, answer.probability);
@@ -763,8 +763,9 @@ class Navigator {
763
763
  page?.setDefaultTimeout(originalTimeout);
764
764
  }
765
765
  const totalAttempted = Math.min(codeBlocks.length, this.verifyAttempts);
766
- const majorityNeeded = Math.floor(totalAttempted / 2) + 1;
767
- let verified = successfulCodes.length >= majorityNeeded;
766
+ let verified = successfulCodes.length > 0 && !failures;
767
+ if (successfulCodes.length && failures)
768
+ verified = await this.provider.oracle(message, { assertions: results.map(({ code, passed, error }) => ({ code, passed, error })) }, { agentName: 'navigator', telemetry: { functionId: 'navigator.verifyState' } });
768
769
  if (alreadyVerified)
769
770
  verified = true;
770
771
  const inexpressible = !alreadyVerified && totalAttempted === 0;
@@ -553,8 +553,10 @@ export class Pilot {
553
553
  const schema = z.object({
554
554
  outcomes: z.array(z.object({
555
555
  expectation: z.string().describe('The expected outcome, repeated exactly as it was given'),
556
- status: z.enum(['passed', 'failed', 'unverified', 'contradiction']).describe('passed = the evidence shows it happened to the thing it names, failed = the evidence shows it did not, unverified = the run never established either way, contradiction = the picture and the run disagree'),
557
- evidence: z.string().nullable().describe('What settled it. For a contradiction, what each side shows. Null when there is nothing to add'),
556
+ status: z
557
+ .enum(['passed', 'failed', 'unverified', 'contradiction'])
558
+ .describe('passed = the evidence shows it happened to the thing it names, failed = the evidence shows the application did not do it, unverified = the run never established either way, contradiction = the picture and the run disagree'),
559
+ evidence: z.string().nullable().describe('What settled it. For unverified, what stopped the run from checking it. For a contradiction, what each side shows. Null when there is nothing to add'),
558
560
  })),
559
561
  });
560
562
  let pageEvidence = '';
@@ -593,10 +595,14 @@ export class Pilot {
593
595
  The log is written in the tester's own words, so an outcome can be satisfied by a step that describes it
594
596
  differently. Judge by what the steps show happened, not by whether the wording matches.
595
597
  Wording may differ, the subject may not. An outcome refers to what the scenario names or chooses; it holds
596
- only when that same thing appears. A different route or a different thing reaching a similar state is "failed",
597
- and so is an outcome about something the run never managed to choose or create.
598
+ only when that same thing appears. A different route or a different thing reaching a similar state is "failed".
599
+ An outcome about something the run never managed to choose or create never passes: it is "failed" when the
600
+ application refused it, "unverified" when the run's own trouble got in the way.
598
601
  Choose "unverified" only when the evidence neither shows the outcome happening nor shows it failing —
599
602
  that is a statement about the run, not about the application.
603
+ "failed" is a statement about the application. When the run's own trouble — an earlier step it could not
604
+ get past, a control it could not reach or operate — left the outcome untested, it is "unverified", and
605
+ the evidence names that blocker.
600
606
  `;
601
607
  const settle = (content, model) => this.provider
602
608
  .generateObject([{ role: 'user', content }], schema, model, {
@@ -18,6 +18,7 @@ import { WithSubPages, getPlannedByStateHash, getRegisteredPlan, registerPlan }
18
18
  import { POSSIBLE_SECTIONS } from "./researcher.js";
19
19
  import { findSimilarStateHash } from "./researcher/cache.js";
20
20
  import { hasFocusedSection } from "./researcher/focus.js";
21
+ import { RulesLoader } from "../utils/rules-loader.js";
21
22
  import { capabilityGroundingRule, dataProtectionRules, fileUploadRule } from "./rules.js";
22
23
  const debugLog = createDebug('explorbot:planner');
23
24
  const TasksSchema = z.object({
@@ -33,6 +34,7 @@ const TasksSchema = z.object({
33
34
  .describe('List of expected outcomes that can be verified. Each outcome should be simple, specific, and easy to check (e.g., "Success message appears", "URL changes to /dashboard", "Form field shows error"). Keep outcomes atomic - do not combine multiple checks into one.'),
34
35
  }))
35
36
  .describe('List of testing scenarios'),
37
+ notPlannableReason: z.string().nullish().describe('When scenarios is empty, one sentence saying why this page has nothing to test; otherwise null.'),
36
38
  });
37
39
  const PlannerBase = WithSessionDedup(WithSubPages(Object));
38
40
  export class Planner extends PlannerBase {
@@ -178,6 +180,9 @@ export class Planner extends PlannerBase {
178
180
  if (!aiResult?.object?.scenarios) {
179
181
  throw new Error('No tasks were created successfully');
180
182
  }
183
+ if (!this.currentPlan && aiResult.object.scenarios.length === 0 && aiResult.object.notPlannableReason) {
184
+ tag('warning').log(`Page is not plannable: ${aiResult.object.notPlannableReason}`);
185
+ }
181
186
  const defaultStartUrl = this.getDefaultStartUrl(state);
182
187
  const fromPlanning = aiResult.object.scenarios.map((s) => new Test(s.scenario, s.priority, s.expectedOutcomes, s.startUrl || defaultStartUrl, s.steps || []));
183
188
  return { tests: fromPlanning, planName: aiResult.object.planName };
@@ -303,11 +308,14 @@ export class Planner extends PlannerBase {
303
308
  conversation.autoTrimTag('tested_scenarios', 10000);
304
309
  conversation.autoTrimTag('docs_context', 8000);
305
310
  conversation.addUserText(this.getSystemMessage(feature));
311
+ let ignoredPagesRule = '';
312
+ if (!feature)
313
+ ignoredPagesRule = `\n${RulesLoader.loadRules('planner', ['ignored-pages'], state.url)}`;
306
314
  const planningPrompt = dedent `
307
315
  <task>
308
316
  Based on the page research, create ${this.MIN_TASKS}-${this.MAX_TASKS} exploratory testing scenarios.
309
317
  For each scenario provide specific steps and expected outcomes.
310
- Exception: if the page reports the requested resource is missing, shows a failure state, or holds no content and no controls, return an empty scenarios list. Never invent tests for a page with nothing to exercise.
318
+ Exception: if the page reports the requested resource is missing, shows a failure state, or holds no content and no controls, return an empty scenarios list. Never invent tests for a page with nothing to exercise.${ignoredPagesRule}
311
319
  </task>
312
320
 
313
321
  <rules>
@@ -3,6 +3,7 @@ import type { ModelMessage } from 'ai';
3
3
  import { type AIConfig } from '../config.js';
4
4
  import { type RetryOptions } from '../utils/retry.js';
5
5
  import { Conversation } from './conversation.js';
6
+ import type { Judge } from './judge.js';
6
7
  declare class AiError extends Error {
7
8
  }
8
9
  export declare class ContextLengthError extends Error {
@@ -10,6 +11,7 @@ export declare class ContextLengthError extends Error {
10
11
  export declare function flushTelemetry(): Promise<void>;
11
12
  export declare class Provider {
12
13
  config: AIConfig;
14
+ judge: Judge | null;
13
15
  telemetryEnabled: boolean;
14
16
  otelSdk: NodeSDK | null;
15
17
  defaultRetryOptions: RetryOptions;
@@ -20,6 +22,8 @@ export declare class Provider {
20
22
  validateConnection(): Promise<void>;
21
23
  stop(): Promise<void>;
22
24
  getModelForAgent(agentName?: string): any;
25
+ setJudge(judge: Judge | null): void;
26
+ oracle(statement: string, state: unknown, options?: any): Promise<boolean>;
23
27
  getAgenticModel(agentName?: string): any;
24
28
  getVisionModel(): any;
25
29
  getConfiguredModels(): Record<string, string>;
@@ -2,11 +2,11 @@ import { AsyncLocalStorage } from 'node:async_hooks';
2
2
  import { OpenTelemetry } from '@ai-sdk/otel';
3
3
  import { LangfuseSpanProcessor } from '@langfuse/otel';
4
4
  import { NodeSDK } from '@opentelemetry/sdk-node';
5
- import { APICallError, NoObjectGeneratedError, asSchema, extractJsonMiddleware, generateObject, generateText, isStepCount, parsePartialJson, registerTelemetry, tool, wrapLanguageModel } from 'ai';
5
+ import { APICallError, NoObjectGeneratedError, NoOutputGeneratedError, Output, asSchema, extractJsonMiddleware, generateText, isStepCount, parsePartialJson, registerTelemetry, tool, wrapLanguageModel } from 'ai';
6
6
  import dedent from 'dedent';
7
7
  import { z } from 'zod';
8
8
  import { clearActivity, setActivity } from "../activity.js";
9
- import { configuredModels, modelName as getModelName } from '../config.js';
9
+ import { PROVIDERS, configuredModels, modelName as getModelName, modelProvider } from '../config.js';
10
10
  import { executionController } from "../execution-controller.js";
11
11
  import { Observability } from "../observability.js";
12
12
  import { Stats } from "../stats.js";
@@ -75,6 +75,7 @@ function combinedAbortSignal(controller) {
75
75
  }
76
76
  export class Provider {
77
77
  config;
78
+ judge = null;
78
79
  telemetryEnabled = false;
79
80
  otelSdk = null;
80
81
  defaultRetryOptions = {
@@ -89,6 +90,7 @@ export class Provider {
89
90
  error.message.includes('AI request timeout') ||
90
91
  error.message.includes('schema') ||
91
92
  error.message.includes('No object generated') ||
93
+ error.message.includes('No output generated') ||
92
94
  error.message.includes('No response text') ||
93
95
  error.message.includes('validate JSON')) &&
94
96
  !error.message.includes('output truncated at maxTokens'));
@@ -112,6 +114,13 @@ export class Provider {
112
114
  });
113
115
  }
114
116
  catch (error) {
117
+ const envKey = PROVIDERS[modelProvider(this.config.model)]?.envKey;
118
+ const apiKey = envKey && process.env[envKey];
119
+ if (envKey && !apiKey)
120
+ throw new AiError(`AI connection failed: ${envKey} is not set. ${error.message}`);
121
+ if (apiKey && [401, 403].includes(error.statusCode)) {
122
+ throw new AiError(`AI connection failed: ${error.message}. ${envKey}=${apiKey.slice(0, 5)}**${apiKey.slice(-3)} was rejected, check the key`);
123
+ }
115
124
  throw new AiError(`AI connection failed: ${error.message}`);
116
125
  }
117
126
  }
@@ -125,6 +134,18 @@ export class Provider {
125
134
  const agentConfig = this.config.agents?.[agentName];
126
135
  return agentConfig?.model || this.config.model;
127
136
  }
137
+ setJudge(judge) {
138
+ this.judge = judge;
139
+ }
140
+ async oracle(statement, state, options = {}) {
141
+ const judged = await this.judge?.assert(statement, state);
142
+ if (typeof judged === 'boolean')
143
+ return judged;
144
+ const schema = z.object({ holds: z.boolean().describe('whether the statement is true') });
145
+ const content = `${statement}\n\n${JSON.stringify(state, null, 2)}`;
146
+ const response = await this.generateObject([{ role: 'user', content }], schema, this.getAgenticModel(options.agentName), options).catch(() => null);
147
+ return response?.object?.holds === true;
148
+ }
128
149
  getAgenticModel(agentName) {
129
150
  if (agentName) {
130
151
  const agentConfig = this.config.agents?.[agentName];
@@ -482,11 +503,12 @@ export class Provider {
482
503
  const modelName = getModelName(modelToUse);
483
504
  setActivity(`🤖 Asking ${modelName} for structured output`, 'ai');
484
505
  promptLog(`Using model: ${modelName}`);
485
- const config = this.buildGenerateConfig({ schema }, { model: modelToUse }, options);
486
506
  try {
507
+ const { timeout, ...config } = this.buildGenerateConfig({ output: Output.object({ schema }) }, { model: modelToUse }, options);
487
508
  promptLog(messages[messages.length - 1].content);
488
509
  const response = await this.withModelRequestSlot(() => withRetry(async () => {
489
- return (await this.raceWithIdleTimeout((signal) => generateObject({ messages, ...config, abortSignal: signal }), config.timeout || 30000));
510
+ const result = (await this.raceWithIdleTimeout((signal) => generateText({ messages, ...config, abortSignal: signal }), timeout || 30000));
511
+ return { object: result.output, usage: result.usage };
490
512
  }, this.getRetryOptions(options)));
491
513
  clearActivity();
492
514
  responseLog(response.object);
@@ -502,7 +524,7 @@ export class Provider {
502
524
  if (Provider.isContextLengthError(error)) {
503
525
  return this.recoverFromContextLength(error, messages, options, (m, o) => this.generateObject(m, schema, model, o));
504
526
  }
505
- if (NoObjectGeneratedError.isInstance(error))
527
+ if (NoObjectGeneratedError.isInstance(error) || NoOutputGeneratedError.isInstance(error))
506
528
  return this.recoverWithPlainJson(messages, schema, modelToUse, options);
507
529
  throw new AiError(error.message || error.toString());
508
530
  }
@@ -1,7 +1,9 @@
1
+ import type { Locator, Page } from 'playwright';
1
2
  import type { ActionResult } from '../action-result.js';
2
3
  import type Explorer from '../explorer.js';
3
4
  import type { StateManager, StateTransition, WebPageState } from '../state-manager.js';
4
5
  import type { Task } from '../test-plan.js';
6
+ import { type UxMeasurements } from '../utils/ux-probe.js';
5
7
  import type { Conversation, ToolExecution } from './conversation.js';
6
8
  import type { Provider } from './provider.js';
7
9
  export declare class Quartermaster {
@@ -10,20 +12,46 @@ export declare class Quartermaster {
10
12
  outputDir: string;
11
13
  pageAnalyses: Map<string, PageAnalysis>;
12
14
  unsubscribe: (() => void) | null;
13
- explorer: Explorer | null;
15
+ explorer: Explorer;
14
16
  pendingAnalyses: Promise<void>[];
15
- constructor(provider: Provider, options?: {
17
+ constructor(provider: Provider, explorer: Explorer, options?: {
16
18
  model?: any;
17
19
  });
18
- start(explorer: Explorer, stateManager: StateManager): void;
20
+ start(stateManager: StateManager): void;
19
21
  stop(): void;
20
22
  onPageChange(event: StateTransition): void;
21
23
  runAxeAnalysis(state: WebPageState): Promise<void>;
22
24
  analyzeSession(task: Task, initialState: ActionResult, conversation: Conversation): Promise<AnalysisReport | null>;
25
+ auditUx(result: ActionResult, context: UxAuditContext): Promise<string>;
23
26
  generateSemanticAnalysis(axeViolations: AxeViolation[], executions: ToolExecution[], initialState: ActionResult): Promise<SemanticIssue[]>;
24
27
  addNotesToTask(task: Task, report: AnalysisReport): void;
25
28
  saveReport(stateHash: string, report: AnalysisReport): void;
26
29
  formatReportMarkdown(report: AnalysisReport): string;
30
+ axeViolations(page: Page, include?: string): Promise<AxeViolation[]>;
31
+ markUxFocus(result: ActionResult, context: UxAuditContext): Promise<UxFocus>;
32
+ pickUxFocus(context: UxAuditContext, mark: (locator: Locator) => Promise<boolean>): Promise<UxFocus | null>;
33
+ collectUxEvidence(page: Page): Promise<UxEvidence>;
34
+ animationsIgnoringReducedMotion(page: Page): Promise<number>;
35
+ captureUxBreakpoint(page: Page): Promise<UxBreakpoint>;
36
+ writeUxReport(result: ActionResult, focus: UxFocus, evidence: UxEvidence, context: UxAuditContext): Promise<string>;
37
+ }
38
+ export interface UxAuditContext {
39
+ focus?: string;
40
+ commands: string[];
41
+ }
42
+ interface UxFocus {
43
+ area: string;
44
+ reason: string;
45
+ }
46
+ interface UxBreakpoint {
47
+ measurements: UxMeasurements;
48
+ aria: string;
49
+ screenshot?: string;
50
+ }
51
+ interface UxEvidence {
52
+ breakpoints: UxBreakpoint[];
53
+ axe: AxeViolation[];
54
+ notes: string[];
27
55
  }
28
56
  interface AxeViolation {
29
57
  id: string;