@supatest/cli 0.0.4 → 0.0.5

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 (69) hide show
  1. package/dist/commands/login.js +392 -0
  2. package/dist/commands/setup.js +234 -0
  3. package/dist/config.js +29 -0
  4. package/dist/core/agent.js +259 -0
  5. package/dist/index.js +154 -6586
  6. package/dist/modes/headless.js +117 -0
  7. package/dist/modes/interactive.js +418 -0
  8. package/dist/presenters/composite.js +32 -0
  9. package/dist/presenters/console.js +163 -0
  10. package/dist/presenters/react.js +217 -0
  11. package/dist/presenters/types.js +1 -0
  12. package/dist/presenters/web.js +78 -0
  13. package/dist/prompts/builder.js +181 -0
  14. package/dist/prompts/fixer.js +148 -0
  15. package/dist/prompts/index.js +3 -0
  16. package/dist/prompts/planner.js +70 -0
  17. package/dist/services/api-client.js +244 -0
  18. package/dist/services/event-streamer.js +130 -0
  19. package/dist/types.js +1 -0
  20. package/dist/ui/App.js +322 -0
  21. package/dist/ui/components/AuthBanner.js +24 -0
  22. package/dist/ui/components/AuthDialog.js +32 -0
  23. package/dist/ui/components/Banner.js +12 -0
  24. package/dist/ui/components/ExpandableSection.js +17 -0
  25. package/dist/ui/components/Header.js +51 -0
  26. package/dist/ui/components/HelpMenu.js +89 -0
  27. package/dist/ui/components/InputPrompt.js +286 -0
  28. package/dist/ui/components/MessageList.js +42 -0
  29. package/dist/ui/components/QueuedMessageDisplay.js +31 -0
  30. package/dist/ui/components/Scrollable.js +103 -0
  31. package/dist/ui/components/SessionSelector.js +196 -0
  32. package/dist/ui/components/StatusBar.js +34 -0
  33. package/dist/ui/components/messages/AssistantMessage.js +20 -0
  34. package/dist/ui/components/messages/ErrorMessage.js +26 -0
  35. package/dist/ui/components/messages/LoadingMessage.js +28 -0
  36. package/dist/ui/components/messages/ThinkingMessage.js +17 -0
  37. package/dist/ui/components/messages/TodoMessage.js +44 -0
  38. package/dist/ui/components/messages/ToolMessage.js +218 -0
  39. package/dist/ui/components/messages/UserMessage.js +14 -0
  40. package/dist/ui/contexts/KeypressContext.js +527 -0
  41. package/dist/ui/contexts/MouseContext.js +98 -0
  42. package/dist/ui/contexts/SessionContext.js +129 -0
  43. package/dist/ui/hooks/useAnimatedScrollbar.js +83 -0
  44. package/dist/ui/hooks/useBatchedScroll.js +22 -0
  45. package/dist/ui/hooks/useBracketedPaste.js +31 -0
  46. package/dist/ui/hooks/useFocus.js +50 -0
  47. package/dist/ui/hooks/useKeypress.js +26 -0
  48. package/dist/ui/hooks/useModeToggle.js +25 -0
  49. package/dist/ui/types/auth.js +13 -0
  50. package/dist/ui/utils/file-completion.js +56 -0
  51. package/dist/ui/utils/input.js +50 -0
  52. package/dist/ui/utils/markdown.js +376 -0
  53. package/dist/ui/utils/mouse.js +189 -0
  54. package/dist/ui/utils/theme.js +59 -0
  55. package/dist/utils/banner.js +9 -0
  56. package/dist/utils/encryption.js +71 -0
  57. package/dist/utils/events.js +36 -0
  58. package/dist/utils/keychain-storage.js +120 -0
  59. package/dist/utils/logger.js +209 -0
  60. package/dist/utils/node-version.js +89 -0
  61. package/dist/utils/plan-file.js +75 -0
  62. package/dist/utils/project-instructions.js +23 -0
  63. package/dist/utils/rich-logger.js +208 -0
  64. package/dist/utils/stdin.js +25 -0
  65. package/dist/utils/stdio.js +80 -0
  66. package/dist/utils/summary.js +94 -0
  67. package/dist/utils/token-storage.js +242 -0
  68. package/dist/version.js +6 -0
  69. package/package.json +3 -4
@@ -0,0 +1,163 @@
1
+ import chalk from "chalk";
2
+ import ora from "ora";
3
+ import { getToolDisplayName } from "shared";
4
+ import { logger } from "../utils/logger";
5
+ import { generateSummary } from "../utils/summary";
6
+ // Fun spinner messages that rotate randomly
7
+ const SPINNER_MESSAGES = [
8
+ "Brainstorming...",
9
+ "Brewing coffee...",
10
+ "Sipping espresso...",
11
+ "Testing theories...",
12
+ "Making magic...",
13
+ "Multiplying matrices...",
14
+ ];
15
+ function getRandomSpinnerMessage() {
16
+ return SPINNER_MESSAGES[Math.floor(Math.random() * SPINNER_MESSAGES.length)];
17
+ }
18
+ function createShimmerFrames(text) {
19
+ const frames = [];
20
+ const baseText = text;
21
+ const spinnerFrames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
22
+ for (let i = 0; i <= baseText.length; i++) {
23
+ const spinnerIcon = spinnerFrames[i % spinnerFrames.length];
24
+ const before = chalk.white(baseText.slice(0, i));
25
+ const current = baseText[i] || "";
26
+ const after = chalk.white(baseText.slice(i + 1));
27
+ const shimmerText = before + chalk.cyan.bold(current) + after;
28
+ frames.push(`${chalk.cyan(spinnerIcon)} ${shimmerText}`);
29
+ }
30
+ return frames;
31
+ }
32
+ export class ConsolePresenter {
33
+ spinner = null;
34
+ verbose;
35
+ stats;
36
+ constructor(options) {
37
+ this.verbose = options.verbose;
38
+ this.stats = {
39
+ startTime: Date.now(),
40
+ iterations: 0,
41
+ filesModified: new Set(),
42
+ commandsRun: [],
43
+ errors: [],
44
+ };
45
+ logger.setVerbose(options.verbose);
46
+ }
47
+ onStart(config) {
48
+ logger.raw("");
49
+ logger.raw(chalk.white.bold("Task:") + " " + chalk.cyan(config.task));
50
+ if (config.logs) {
51
+ logger.info("Processing provided logs...");
52
+ }
53
+ logger.raw("");
54
+ this.startSpinner();
55
+ }
56
+ onLog(message) {
57
+ if (this.verbose) {
58
+ this.stopSpinner();
59
+ logger.debug(message);
60
+ this.startSpinner();
61
+ }
62
+ }
63
+ onAssistantText(text) {
64
+ this.stopSpinner();
65
+ logger.raw(text);
66
+ this.startSpinner();
67
+ }
68
+ onThinking(text) {
69
+ if (this.verbose) {
70
+ this.stopSpinner();
71
+ logger.debug(`Thinking: ${text}`);
72
+ this.startSpinner();
73
+ }
74
+ }
75
+ onToolUse(tool, input, _toolId) {
76
+ if (this.spinner) {
77
+ const displayName = getToolDisplayName(tool);
78
+ this.spinner.text = `Using ${displayName}...`;
79
+ }
80
+ this.stopSpinner();
81
+ if (tool === "Read") {
82
+ logger.toolRead(input?.file_path || "file");
83
+ }
84
+ else if (tool === "Write") {
85
+ const filePath = input?.file_path;
86
+ if (filePath) {
87
+ this.stats.filesModified.add(filePath);
88
+ logger.toolWrite(filePath);
89
+ }
90
+ }
91
+ else if (tool === "Edit") {
92
+ const filePath = input?.file_path;
93
+ if (filePath) {
94
+ this.stats.filesModified.add(filePath);
95
+ logger.toolEdit(filePath);
96
+ }
97
+ }
98
+ else if (tool === "Bash") {
99
+ const command = input?.command;
100
+ if (command) {
101
+ this.stats.commandsRun.push(command);
102
+ const shortCmd = command.length > 60 ? `${command.substring(0, 60)}...` : command;
103
+ logger.toolBash(shortCmd);
104
+ }
105
+ }
106
+ else if (tool === "Glob") {
107
+ logger.toolSearch("files", input?.pattern || "");
108
+ }
109
+ else if (tool === "Grep") {
110
+ logger.toolSearch("code", input?.pattern || "");
111
+ }
112
+ else if (tool === "Task") {
113
+ logger.toolAgent(input?.subagent_type || "task");
114
+ }
115
+ else if (tool === "TodoWrite") {
116
+ const todos = input?.todos;
117
+ if (Array.isArray(todos)) {
118
+ logger.todoUpdate(todos);
119
+ }
120
+ else {
121
+ logger.info("📝 Updated todos");
122
+ }
123
+ }
124
+ else {
125
+ logger.debug(`🔧 Using tool: ${tool}`);
126
+ }
127
+ logger.raw("");
128
+ this.startSpinner();
129
+ }
130
+ onTurnComplete(_content) {
131
+ // No-op for console - we handle output in individual callbacks
132
+ }
133
+ onError(error) {
134
+ this.stopSpinner();
135
+ logger.error(error);
136
+ this.stats.errors.push(error);
137
+ }
138
+ onComplete(result) {
139
+ this.stopSpinner();
140
+ this.stats.endTime = Date.now();
141
+ this.stats.iterations = result.iterations;
142
+ const summaryText = generateSummary(this.stats, result, this.verbose);
143
+ logger.raw(summaryText);
144
+ }
145
+ startSpinner() {
146
+ if (!this.spinner && !logger.isSilent()) {
147
+ const message = getRandomSpinnerMessage();
148
+ this.spinner = ora({
149
+ spinner: {
150
+ interval: 80,
151
+ frames: createShimmerFrames(message),
152
+ },
153
+ });
154
+ this.spinner.start();
155
+ }
156
+ }
157
+ stopSpinner() {
158
+ if (this.spinner) {
159
+ this.spinner.stop();
160
+ this.spinner = null;
161
+ }
162
+ }
163
+ }
@@ -0,0 +1,217 @@
1
+ import { getToolDisplayName } from "shared";
2
+ /**
3
+ * Get human-readable description for tool call
4
+ */
5
+ function getToolDescription(toolName, input) {
6
+ switch (toolName) {
7
+ case "Read":
8
+ return input?.file_path || "file";
9
+ case "Write":
10
+ return input?.file_path || "file";
11
+ case "Edit":
12
+ return input?.file_path || "file";
13
+ case "Bash": {
14
+ const cmd = input?.command || "";
15
+ return cmd.length > 60 ? `${cmd.substring(0, 60)}...` : cmd;
16
+ }
17
+ case "Glob":
18
+ return `pattern: "${input?.pattern || "files"}"`;
19
+ case "Grep": {
20
+ const pattern = input?.pattern || "code";
21
+ const path = input?.path;
22
+ return path ? `"${pattern}" (in ${path})` : `"${pattern}"`;
23
+ }
24
+ case "Task":
25
+ return input?.subagent_type || "task";
26
+ case "TodoWrite":
27
+ return "Updated todos";
28
+ case "BashOutput":
29
+ case "Command Output":
30
+ return input?.bash_id || "shell output";
31
+ default:
32
+ return toolName;
33
+ }
34
+ }
35
+ export class ReactPresenter {
36
+ callbacks;
37
+ apiClient;
38
+ sessionId;
39
+ verbose;
40
+ // Track message state for combining consecutive blocks
41
+ hasAssistantMessage = false;
42
+ currentAssistantText = "";
43
+ hasThinkingMessage = false;
44
+ currentThinkingText = "";
45
+ constructor(callbacks, apiClient, sessionId, verbose = false) {
46
+ this.callbacks = callbacks;
47
+ this.apiClient = apiClient;
48
+ this.sessionId = sessionId;
49
+ this.verbose = verbose;
50
+ }
51
+ async onStart(config) {
52
+ // Send initial user message event to API
53
+ const userMessageEvent = {
54
+ type: "user_message",
55
+ content: [{ type: "text", text: config.task }],
56
+ };
57
+ await this.apiClient.streamEvent(this.sessionId, userMessageEvent);
58
+ }
59
+ onLog(message) {
60
+ if (this.verbose) {
61
+ console.error(message);
62
+ }
63
+ }
64
+ async onAssistantText(text) {
65
+ if (!this.hasAssistantMessage) {
66
+ // First text block - create new message
67
+ this.callbacks.addMessage({
68
+ type: "assistant",
69
+ content: text,
70
+ isPending: false,
71
+ });
72
+ this.hasAssistantMessage = true;
73
+ this.currentAssistantText = text;
74
+ }
75
+ else {
76
+ // Append to current message
77
+ this.currentAssistantText += text;
78
+ this.callbacks.updateLastMessage({
79
+ content: this.currentAssistantText,
80
+ });
81
+ }
82
+ // Stream to API
83
+ const textEvent = {
84
+ type: "assistant_text",
85
+ delta: text,
86
+ };
87
+ await this.apiClient.streamEvent(this.sessionId, textEvent);
88
+ }
89
+ async onThinking(text) {
90
+ if (!this.hasThinkingMessage) {
91
+ // Create new thinking message
92
+ this.callbacks.addMessage({
93
+ type: "thinking",
94
+ content: text,
95
+ isExpanded: false,
96
+ });
97
+ this.hasThinkingMessage = true;
98
+ this.currentThinkingText = text;
99
+ }
100
+ else {
101
+ // Append to existing thinking message
102
+ this.currentThinkingText += text;
103
+ this.callbacks.updateLastMessage({
104
+ content: this.currentThinkingText,
105
+ });
106
+ }
107
+ // Stream to API
108
+ const thinkingEvent = {
109
+ type: "assistant_thinking",
110
+ delta: text,
111
+ };
112
+ await this.apiClient.streamEvent(this.sessionId, thinkingEvent);
113
+ }
114
+ async onToolUse(tool, input, toolId) {
115
+ // Add tool message to UI
116
+ const message = {
117
+ type: "tool",
118
+ content: getToolDescription(tool, input),
119
+ toolName: getToolDisplayName(tool),
120
+ toolInput: input,
121
+ toolResult: undefined,
122
+ isExpanded: false,
123
+ toolUseId: toolId,
124
+ };
125
+ this.callbacks.addMessage(message);
126
+ // Reset message state - next text/thinking creates new messages
127
+ this.hasAssistantMessage = false;
128
+ this.hasThinkingMessage = false;
129
+ this.currentAssistantText = "";
130
+ this.currentThinkingText = "";
131
+ // Update stats for file modifications
132
+ if (tool === "Write" || tool === "Edit") {
133
+ const filePath = input?.file_path;
134
+ if (filePath) {
135
+ this.callbacks.updateStats({
136
+ filesModified: new Set([filePath]),
137
+ });
138
+ }
139
+ }
140
+ else if (tool === "Bash") {
141
+ const command = input?.command;
142
+ if (command) {
143
+ this.callbacks.updateStats({
144
+ commandsRun: [command],
145
+ });
146
+ }
147
+ }
148
+ else if (tool === "TodoWrite") {
149
+ const todos = input?.todos;
150
+ if (todos && Array.isArray(todos)) {
151
+ this.callbacks.setTodos(todos);
152
+ this.callbacks.addMessage({
153
+ type: "todo",
154
+ content: "",
155
+ todos: todos,
156
+ });
157
+ }
158
+ }
159
+ // Stream tool use event to API
160
+ const toolUseEvent = {
161
+ type: "tool_use",
162
+ id: toolId,
163
+ name: tool,
164
+ input: input || {},
165
+ };
166
+ await this.apiClient.streamEvent(this.sessionId, toolUseEvent);
167
+ }
168
+ async onToolResult(toolId, result) {
169
+ // Update the tool message with the result
170
+ this.callbacks.updateMessageByToolId(toolId, {
171
+ toolResult: result,
172
+ });
173
+ // Stream tool result to API
174
+ const toolResultEvent = {
175
+ type: "tool_result",
176
+ tool_use_id: toolId,
177
+ content: result,
178
+ };
179
+ await this.apiClient.streamEvent(this.sessionId, toolResultEvent);
180
+ }
181
+ async onTurnComplete(content) {
182
+ // Stream message complete to API
183
+ const messageCompleteEvent = {
184
+ type: "message_complete",
185
+ message: {
186
+ role: "assistant",
187
+ content: content,
188
+ },
189
+ };
190
+ await this.apiClient.streamEvent(this.sessionId, messageCompleteEvent);
191
+ }
192
+ async onError(error) {
193
+ this.callbacks.addMessage({
194
+ type: "error",
195
+ content: error,
196
+ errorType: "error",
197
+ });
198
+ await this.apiClient.streamEvent(this.sessionId, {
199
+ type: "session_error",
200
+ error: error,
201
+ });
202
+ }
203
+ async onComplete(result) {
204
+ if (result.success) {
205
+ await this.apiClient.streamEvent(this.sessionId, {
206
+ type: "session_complete",
207
+ });
208
+ }
209
+ else {
210
+ await this.apiClient.streamEvent(this.sessionId, {
211
+ type: "session_error",
212
+ error: result.error || "Unknown error",
213
+ });
214
+ }
215
+ this.callbacks.onComplete(result.success);
216
+ }
217
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,78 @@
1
+ import { EventStreamer } from "../services/event-streamer";
2
+ export class WebPresenter {
3
+ streamer;
4
+ constructor(apiClient, sessionId) {
5
+ this.streamer = new EventStreamer(apiClient, sessionId);
6
+ }
7
+ async onStart(config) {
8
+ // Send the initial user message to establish context
9
+ const event = {
10
+ type: "user_message",
11
+ content: [
12
+ {
13
+ type: "text",
14
+ text: config.task,
15
+ },
16
+ ],
17
+ };
18
+ await this.streamer.queueEvent(event);
19
+ }
20
+ onLog(_message) {
21
+ // Debug logs are not streamed to web
22
+ }
23
+ async onAssistantText(text) {
24
+ const event = {
25
+ type: "assistant_text",
26
+ delta: text,
27
+ };
28
+ await this.streamer.queueEvent(event);
29
+ }
30
+ async onThinking(text) {
31
+ const event = {
32
+ type: "assistant_thinking",
33
+ delta: text,
34
+ };
35
+ await this.streamer.queueEvent(event);
36
+ }
37
+ async onToolUse(tool, input, toolId) {
38
+ const event = {
39
+ type: "tool_use",
40
+ id: toolId,
41
+ name: tool,
42
+ input: input || {},
43
+ };
44
+ await this.streamer.queueEvent(event);
45
+ }
46
+ async onTurnComplete(content) {
47
+ // Flush pending events first
48
+ await this.streamer.flush();
49
+ const event = {
50
+ type: "message_complete",
51
+ message: {
52
+ role: "assistant",
53
+ content: content,
54
+ },
55
+ };
56
+ await this.streamer.queueEvent(event);
57
+ }
58
+ async onError(error) {
59
+ const event = {
60
+ type: "session_error",
61
+ error: error,
62
+ };
63
+ await this.streamer.queueEvent(event);
64
+ await this.streamer.shutdown();
65
+ }
66
+ async onComplete(result) {
67
+ if (result.success) {
68
+ await this.streamer.queueEvent({ type: "session_complete" });
69
+ }
70
+ else {
71
+ await this.streamer.queueEvent({
72
+ type: "session_error",
73
+ error: result.error || "Unknown error",
74
+ });
75
+ }
76
+ await this.streamer.shutdown();
77
+ }
78
+ }
@@ -0,0 +1,181 @@
1
+ export const builderPrompt = `<role>
2
+ You are an E2E Test Builder Agent that iteratively creates, runs, and fixes Playwright tests until they pass. You have access to Playwright MCP tools for browser automation and debugging.
3
+ </role>
4
+
5
+ <core_workflow>
6
+ Follow this iterative build loop for each test:
7
+
8
+ 1. **Discover** - Understand project setup before writing (see discovery section)
9
+ 2. **Understand** - Read the test spec or user flow requirements
10
+ 3. **Write** - Create or update the Playwright test file
11
+ 4. **Run** - Execute the test using the correct command
12
+ 5. **Verify** - Check results; if passing, move to next test
13
+ 6. **Debug** - If failing, use Playwright MCP tools to investigate
14
+ 7. **Fix** - Update test based on findings, return to step 4
15
+
16
+ Continue until all tests pass. Do NOT stop after first failure. Max 5 attempts per test.
17
+ </core_workflow>
18
+
19
+ <discovery>
20
+ Before writing tests, understand the project setup:
21
+
22
+ **Test infrastructure:**
23
+ - Check package.json for test scripts and playwright dependency
24
+ - Look for playwright.config.ts or playwright.config.js
25
+ - Find existing test directory (tests/, e2e/, __tests__/)
26
+ - Note any existing test patterns or fixtures
27
+
28
+ **Application structure:**
29
+ - Identify the base URL (from config or package.json scripts)
30
+ - Find main routes/pages in the app
31
+ - Check for authentication requirements
32
+
33
+ **Existing patterns:**
34
+ - Look at existing tests for selector conventions
35
+ - Check for shared fixtures or page objects
36
+ - Note any custom test utilities
37
+
38
+ **If no Playwright setup exists:**
39
+ - Initialize with \`npm init playwright@latest\`
40
+ - Use defaults unless user specifies otherwise
41
+
42
+ **If existing tests exist:**
43
+ - Follow their patterns and conventions
44
+ - Use the same directory structure
45
+ - Reuse existing fixtures and utilities
46
+ </discovery>
47
+
48
+ <test_data_strategy>
49
+ **Prefer API setup when available, fall back to UI otherwise.**
50
+
51
+ - API setup is faster and more reliable for creating test data
52
+ - Use UI setup when no API is available
53
+ - Each test should create its own data
54
+ - Clean up after tests when possible
55
+ - Use unique identifiers (timestamps, random strings) to avoid collisions
56
+ </test_data_strategy>
57
+
58
+ <playwright_execution>
59
+ CRITICAL: Always run Playwright tests correctly to ensure clean exits.
60
+
61
+ **Correct test commands:**
62
+ - Single test: \`npx playwright test tests/example.spec.ts --reporter=list\`
63
+ - All tests: \`npx playwright test --reporter=list\`
64
+ - Headed mode (debugging): \`npx playwright test --headed --reporter=list\`
65
+
66
+ **NEVER use:**
67
+ - \`--ui\` flag (opens interactive UI that blocks)
68
+ - \`--reporter=html\` without \`--reporter=list\` (may open server)
69
+ - Commands without \`--reporter=list\` in CI/headless mode
70
+
71
+ **Process management:**
72
+ - Always use \`--reporter=list\` or \`--reporter=dot\` for clean output
73
+ - Tests should exit automatically after completion
74
+ - If a process hangs, kill it and retry with correct flags
75
+ </playwright_execution>
76
+
77
+ <debugging_with_mcp>
78
+ When tests fail, use Playwright MCP tools to investigate:
79
+
80
+ 1. **Navigate**: Use \`mcp__playwright__playwright_navigate\` to load the failing page
81
+ 2. **Inspect DOM**: Use \`mcp__playwright__playwright_get_visible_html\` to see actual elements
82
+ 3. **Screenshot**: Use \`mcp__playwright__playwright_screenshot\` to capture current state
83
+ 4. **Console logs**: Use \`mcp__playwright__playwright_console_logs\` to check for JS errors
84
+ 5. **Interact**: Use click/fill tools to manually reproduce the flow
85
+
86
+ **Workflow**: Navigate → inspect HTML → verify selectors → check console → fix
87
+ </debugging_with_mcp>
88
+
89
+ <selector_strategy>
90
+ Prioritize resilient selectors:
91
+ 1. \`getByRole()\` - accessibility-focused, most stable
92
+ 2. \`getByLabel()\` - form elements
93
+ 3. \`getByText()\` - user-visible content
94
+ 4. \`getByTestId()\` - explicit test markers
95
+ 5. CSS selectors - last resort, avoid class-based
96
+
97
+ When selectors fail:
98
+ - Use MCP to inspect actual DOM structure
99
+ - Check if element exists but has different text/role
100
+ - Verify element is visible and not hidden
101
+ </selector_strategy>
102
+
103
+ <test_structure>
104
+ Use Arrange-Act-Assert pattern:
105
+ \`\`\`typescript
106
+ test('should complete checkout', async ({ page }) => {
107
+ // Arrange - Setup preconditions
108
+ await page.goto('/cart');
109
+
110
+ // Act - Perform the action
111
+ await page.getByRole('button', { name: 'Checkout' }).click();
112
+ await page.getByLabel('Card number').fill('4242424242424242');
113
+ await page.getByRole('button', { name: 'Pay' }).click();
114
+
115
+ // Assert - Verify outcomes
116
+ await expect(page).toHaveURL(/\\/confirmation/);
117
+ await expect(page.getByText('Order confirmed')).toBeVisible();
118
+ });
119
+ \`\`\`
120
+ </test_structure>
121
+
122
+ <anti_patterns>
123
+ Avoid these common mistakes:
124
+
125
+ - \`waitForTimeout()\` - use explicit element waits instead
126
+ - Brittle CSS class selectors - use role/label/testid
127
+ - Tests depending on execution order - each test must be independent
128
+ - Shared test data between tests - create fresh data per test
129
+ - Vague assertions like \`toBeTruthy()\` - be specific
130
+ - Hard-coded delays for animations - wait for element state
131
+ - Too many assertions per test - test one logical flow
132
+ - No cleanup in afterEach/afterAll - clean up test data
133
+ </anti_patterns>
134
+
135
+ <iteration_mindset>
136
+ Expect multiple iterations. This is normal and efficient:
137
+ - First attempt: Write test based on understanding
138
+ - Second: Fix selector issues found during run
139
+ - Third: Handle timing/async issues
140
+ - Fourth+: Edge cases and refinements
141
+
142
+ Keep iterating until green. Three robust passing tests are better than ten flaky ones.
143
+ </iteration_mindset>
144
+
145
+ <decision_gates>
146
+ **Keep building (proceed autonomously):**
147
+ - Test fails with clear selector/timing issue → fix and retry
148
+ - Missing test file → create it
149
+ - Standard patterns (forms, navigation, CRUD) → just build
150
+ - Error message is actionable → iterate on fix
151
+
152
+ **Ask user first:**
153
+ - Ambiguous requirements ("test the dashboard" - which parts?)
154
+ - Multiple valid approaches (shared fixture vs per-test setup?)
155
+ - Missing infrastructure (no playwright config, no test directory)
156
+ - Authentication unclear (how do users log in? test account?)
157
+ - External dependencies (tests need API keys, seeds, third-party services)
158
+
159
+ **Stop and report:**
160
+ - App bug discovered (test is correct, app is broken)
161
+ - Max attempts reached (5 attempts with no progress)
162
+ - Blocked by environment (app not running, wrong URL)
163
+ - Test requires unavailable capabilities (mobile, specific browser)
164
+ </decision_gates>
165
+
166
+ <definition_of_done>
167
+ Before marking a test complete:
168
+ - [ ] Test passes consistently (2+ runs)
169
+ - [ ] No flaky behavior detected
170
+ - [ ] Test data is cleaned up (or isolated)
171
+ - [ ] Selectors are resilient (not class-based)
172
+ - [ ] No arbitrary timeouts used
173
+ </definition_of_done>
174
+
175
+ <communication>
176
+ When reporting progress:
177
+ - State which test is being worked on
178
+ - Report pass/fail status after each run
179
+ - When fixing, explain what was wrong and the fix
180
+ - Summarize final status: X/Y tests passing
181
+ </communication>`;