@mseep/ai-tech-app-agent 1.0.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/.env +24 -0
- package/.env.example +24 -0
- package/Jenkinsfile +210 -0
- package/MCP-SERVER-GUIDE.md +405 -0
- package/README.MD +450 -0
- package/dist/config/app.config.d.ts +65 -0
- package/dist/config/app.config.d.ts.map +1 -0
- package/dist/config/app.config.js +94 -0
- package/dist/config/app.config.js.map +1 -0
- package/dist/config/llm.config.d.ts +63 -0
- package/dist/config/llm.config.d.ts.map +1 -0
- package/dist/config/llm.config.js +158 -0
- package/dist/config/llm.config.js.map +1 -0
- package/dist/config/mcp.config.d.ts +175 -0
- package/dist/config/mcp.config.d.ts.map +1 -0
- package/dist/config/mcp.config.js +215 -0
- package/dist/config/mcp.config.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +175 -0
- package/dist/index.js.map +1 -0
- package/dist/llm/llamaClient.d.ts +14 -0
- package/dist/llm/llamaClient.d.ts.map +1 -0
- package/dist/llm/llamaClient.js +136 -0
- package/dist/llm/llamaClient.js.map +1 -0
- package/dist/mcp/mcpClient.d.ts +132 -0
- package/dist/mcp/mcpClient.d.ts.map +1 -0
- package/dist/mcp/mcpClient.js +784 -0
- package/dist/mcp/mcpClient.js.map +1 -0
- package/dist/models/testSpec.d.ts +78 -0
- package/dist/models/testSpec.d.ts.map +1 -0
- package/dist/models/testSpec.js +3 -0
- package/dist/models/testSpec.js.map +1 -0
- package/dist/orchestrator/aiTestRunner.d.ts +18 -0
- package/dist/orchestrator/aiTestRunner.d.ts.map +1 -0
- package/dist/orchestrator/aiTestRunner.js +247 -0
- package/dist/orchestrator/aiTestRunner.js.map +1 -0
- package/dist/utils/logger.d.ts +4 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +49 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/promptBuilder.d.ts +62 -0
- package/dist/utils/promptBuilder.d.ts.map +1 -0
- package/dist/utils/promptBuilder.js +333 -0
- package/dist/utils/promptBuilder.js.map +1 -0
- package/knowledge/app-knowledge.txt +100 -0
- package/logs/combined.log +486 -0
- package/logs/error.log +50 -0
- package/package.json +62 -0
- package/reports/screenshots/screenshot_1764535110518.png +0 -0
- package/reports/test-report.json +106 -0
- package/scripts/check-mcp-server.sh +100 -0
- package/scripts/extract-pom-knowledge.js +222 -0
- package/scripts/pre-test-setup.js +262 -0
- package/scripts/start-mcp-server.sh +76 -0
- package/src/config/app.config.ts +175 -0
- package/src/config/llm.config.ts +220 -0
- package/src/config/mcp.config.ts +291 -0
- package/src/index.ts +161 -0
- package/src/llm/llamaClient.ts +159 -0
- package/src/mcp/mcpClient.ts +878 -0
- package/src/models/testSpec.ts +85 -0
- package/src/orchestrator/aiTestRunner.ts +286 -0
- package/src/utils/logger.ts +59 -0
- package/src/utils/promptBuilder.ts +384 -0
- package/tests/nlp-specs/login-flow.yaml +31 -0
- package/tsconfig.json +31 -0
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.PromptBuilder = void 0;
|
|
37
|
+
const llm_config_1 = __importStar(require("../config/llm.config"));
|
|
38
|
+
class PromptBuilder {
|
|
39
|
+
constructor() {
|
|
40
|
+
this.maxUIElements = llm_config_1.default.prompt.maxUIElements;
|
|
41
|
+
this.maxContextTokens = llm_config_1.default.prompt.maxContextTokens;
|
|
42
|
+
this.includeAppContext = llm_config_1.default.prompt.includeAppContext;
|
|
43
|
+
this.includeUIContext = llm_config_1.default.prompt.includeUIContext;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Build the main action planning prompt
|
|
47
|
+
*/
|
|
48
|
+
buildActionPlanningPrompt(stepText, uiContext, appKnowledge, platform) {
|
|
49
|
+
const sections = [];
|
|
50
|
+
// System context
|
|
51
|
+
sections.push(llm_config_1.PromptTemplates.actionPlanning);
|
|
52
|
+
sections.push('');
|
|
53
|
+
// App-specific knowledge
|
|
54
|
+
if (this.includeAppContext && appKnowledge) {
|
|
55
|
+
sections.push('**App-Specific Knowledge:**');
|
|
56
|
+
sections.push(this.truncateText(appKnowledge, 1000));
|
|
57
|
+
sections.push('');
|
|
58
|
+
}
|
|
59
|
+
// Platform information
|
|
60
|
+
if (platform) {
|
|
61
|
+
sections.push(`**Platform:** ${platform.toUpperCase()}`);
|
|
62
|
+
sections.push('');
|
|
63
|
+
}
|
|
64
|
+
// Current UI state
|
|
65
|
+
if (this.includeUIContext) {
|
|
66
|
+
sections.push('**Current UI State:**');
|
|
67
|
+
sections.push(this.formatUIContext(uiContext));
|
|
68
|
+
sections.push('');
|
|
69
|
+
}
|
|
70
|
+
// Test step to execute
|
|
71
|
+
sections.push('**Test Step to Execute:**');
|
|
72
|
+
sections.push(`"${stepText}"`);
|
|
73
|
+
sections.push('');
|
|
74
|
+
// Output format instructions
|
|
75
|
+
sections.push(this.getOutputFormatInstructions());
|
|
76
|
+
return sections.join('\n');
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Build prompt for context analysis
|
|
80
|
+
*/
|
|
81
|
+
buildContextAnalysisPrompt(uiContext, focusArea) {
|
|
82
|
+
const sections = [];
|
|
83
|
+
sections.push(llm_config_1.PromptTemplates.contextAnalysis);
|
|
84
|
+
sections.push('');
|
|
85
|
+
if (focusArea) {
|
|
86
|
+
sections.push(`**Focus Area:** ${focusArea}`);
|
|
87
|
+
sections.push('');
|
|
88
|
+
}
|
|
89
|
+
sections.push('**Current UI State:**');
|
|
90
|
+
sections.push(this.formatUIContext(uiContext));
|
|
91
|
+
sections.push('');
|
|
92
|
+
sections.push('Identify and list the most relevant interactive elements with their best selectors.');
|
|
93
|
+
return sections.join('\n');
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Build prompt for error recovery
|
|
97
|
+
*/
|
|
98
|
+
buildErrorRecoveryPrompt(stepText, previousError, uiContext, attemptNumber) {
|
|
99
|
+
const sections = [];
|
|
100
|
+
sections.push(llm_config_1.PromptTemplates.errorRecovery);
|
|
101
|
+
sections.push('');
|
|
102
|
+
sections.push(`**Original Test Step:** "${stepText}"`);
|
|
103
|
+
sections.push(`**Attempt Number:** ${attemptNumber}`);
|
|
104
|
+
sections.push(`**Previous Error:** ${previousError}`);
|
|
105
|
+
sections.push('');
|
|
106
|
+
sections.push('**Current UI State:**');
|
|
107
|
+
sections.push(this.formatUIContext(uiContext));
|
|
108
|
+
sections.push('');
|
|
109
|
+
sections.push('**Instructions:**');
|
|
110
|
+
sections.push('1. Analyze why the previous attempt failed');
|
|
111
|
+
sections.push('2. Check if the UI state has changed');
|
|
112
|
+
sections.push('3. Suggest alternative actions or selectors');
|
|
113
|
+
sections.push('4. If the step is impossible, return an empty actions array with reasoning');
|
|
114
|
+
sections.push('');
|
|
115
|
+
sections.push(this.getOutputFormatInstructions());
|
|
116
|
+
return sections.join('\n');
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Build prompt for assertion generation
|
|
120
|
+
*/
|
|
121
|
+
buildAssertionPrompt(expectedOutcome, uiContext) {
|
|
122
|
+
const sections = [];
|
|
123
|
+
sections.push(llm_config_1.PromptTemplates.assertionGeneration);
|
|
124
|
+
sections.push('');
|
|
125
|
+
sections.push(`**Expected Outcome:** "${expectedOutcome}"`);
|
|
126
|
+
sections.push('');
|
|
127
|
+
sections.push('**Current UI State:**');
|
|
128
|
+
sections.push(this.formatUIContext(uiContext));
|
|
129
|
+
sections.push('');
|
|
130
|
+
sections.push('Generate assertion actions to verify the expected outcome.');
|
|
131
|
+
sections.push('');
|
|
132
|
+
sections.push(this.getOutputFormatInstructions());
|
|
133
|
+
return sections.join('\n');
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Format UI context for inclusion in prompts
|
|
137
|
+
*/
|
|
138
|
+
formatUIContext(uiContext) {
|
|
139
|
+
const lines = [];
|
|
140
|
+
// Current screen/activity
|
|
141
|
+
if (uiContext.currentActivity) {
|
|
142
|
+
lines.push(`Current Screen: ${uiContext.currentActivity}`);
|
|
143
|
+
lines.push('');
|
|
144
|
+
}
|
|
145
|
+
// Visible elements
|
|
146
|
+
const elements = uiContext.visibleElements || [];
|
|
147
|
+
if (elements.length === 0) {
|
|
148
|
+
lines.push('No visible elements detected.');
|
|
149
|
+
return lines.join('\n');
|
|
150
|
+
}
|
|
151
|
+
lines.push(`Visible Elements (showing ${Math.min(elements.length, this.maxUIElements)} of ${elements.length}):`);
|
|
152
|
+
lines.push('');
|
|
153
|
+
// Filter and sort elements by relevance
|
|
154
|
+
const relevantElements = this.filterRelevantElements(elements);
|
|
155
|
+
const topElements = relevantElements.slice(0, this.maxUIElements);
|
|
156
|
+
topElements.forEach((element, index) => {
|
|
157
|
+
lines.push(this.formatElement(element, index + 1));
|
|
158
|
+
});
|
|
159
|
+
return lines.join('\n');
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Format a single UI element
|
|
163
|
+
*/
|
|
164
|
+
formatElement(element, index) {
|
|
165
|
+
const parts = [`${index}.`];
|
|
166
|
+
// Element type
|
|
167
|
+
parts.push(`[${element.type}]`);
|
|
168
|
+
// Text content
|
|
169
|
+
if (element.text && element.text.trim().length > 0) {
|
|
170
|
+
parts.push(`text="${this.truncateText(element.text, 50)}"`);
|
|
171
|
+
}
|
|
172
|
+
// Resource ID (Android) or Name (iOS)
|
|
173
|
+
if (element.resourceId) {
|
|
174
|
+
const shortId = element.resourceId.split('/').pop() || element.resourceId;
|
|
175
|
+
parts.push(`id="${shortId}"`);
|
|
176
|
+
}
|
|
177
|
+
// Content description / accessibility label
|
|
178
|
+
if (element.contentDesc) {
|
|
179
|
+
parts.push(`desc="${this.truncateText(element.contentDesc, 30)}"`);
|
|
180
|
+
}
|
|
181
|
+
// Attributes
|
|
182
|
+
const attrs = [];
|
|
183
|
+
if (element.clickable)
|
|
184
|
+
attrs.push('clickable');
|
|
185
|
+
if (element.enabled)
|
|
186
|
+
attrs.push('enabled');
|
|
187
|
+
if (!element.enabled)
|
|
188
|
+
attrs.push('disabled');
|
|
189
|
+
if (attrs.length > 0) {
|
|
190
|
+
parts.push(`[${attrs.join(', ')}]`);
|
|
191
|
+
}
|
|
192
|
+
// Bounds (if useful)
|
|
193
|
+
if (element.bounds) {
|
|
194
|
+
parts.push(`bounds=${element.bounds}`);
|
|
195
|
+
}
|
|
196
|
+
return parts.join(' ');
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Filter elements to show most relevant ones
|
|
200
|
+
*/
|
|
201
|
+
filterRelevantElements(elements) {
|
|
202
|
+
// Prioritize elements that are:
|
|
203
|
+
// 1. Interactive (clickable, editable)
|
|
204
|
+
// 2. Have meaningful text or labels
|
|
205
|
+
// 3. Are common UI patterns (buttons, inputs, etc.)
|
|
206
|
+
const scored = elements.map(element => ({
|
|
207
|
+
element,
|
|
208
|
+
score: this.calculateElementRelevance(element),
|
|
209
|
+
}));
|
|
210
|
+
// Sort by score (descending)
|
|
211
|
+
scored.sort((a, b) => b.score - a.score);
|
|
212
|
+
return scored.map(s => s.element);
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Calculate relevance score for an element
|
|
216
|
+
*/
|
|
217
|
+
calculateElementRelevance(element) {
|
|
218
|
+
let score = 0;
|
|
219
|
+
// Interactive elements get high priority
|
|
220
|
+
if (element.clickable)
|
|
221
|
+
score += 10;
|
|
222
|
+
// Elements with text are usually important
|
|
223
|
+
if (element.text && element.text.trim().length > 0) {
|
|
224
|
+
score += 8;
|
|
225
|
+
}
|
|
226
|
+
// Elements with IDs are identifiable
|
|
227
|
+
if (element.resourceId)
|
|
228
|
+
score += 5;
|
|
229
|
+
// Elements with accessibility labels
|
|
230
|
+
if (element.contentDesc)
|
|
231
|
+
score += 5;
|
|
232
|
+
// Common interactive types
|
|
233
|
+
const interactiveTypes = [
|
|
234
|
+
'Button', 'EditText', 'TextView', 'ImageButton',
|
|
235
|
+
'CheckBox', 'RadioButton', 'Switch', 'Spinner',
|
|
236
|
+
'UIButton', 'UITextField', 'UITextView', 'UILabel',
|
|
237
|
+
];
|
|
238
|
+
if (interactiveTypes.some(type => element.type.includes(type))) {
|
|
239
|
+
score += 7;
|
|
240
|
+
}
|
|
241
|
+
// Enabled elements
|
|
242
|
+
if (element.enabled)
|
|
243
|
+
score += 3;
|
|
244
|
+
// Penalize common container types (unless they have text)
|
|
245
|
+
const containerTypes = ['ViewGroup', 'LinearLayout', 'FrameLayout', 'View', 'UIView'];
|
|
246
|
+
if (containerTypes.some(type => element.type.includes(type)) && !element.text) {
|
|
247
|
+
score -= 5;
|
|
248
|
+
}
|
|
249
|
+
return Math.max(0, score);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* Get output format instructions
|
|
253
|
+
*/
|
|
254
|
+
getOutputFormatInstructions() {
|
|
255
|
+
return `**Output Format (JSON only, no markdown):**
|
|
256
|
+
{
|
|
257
|
+
"actions": [
|
|
258
|
+
{
|
|
259
|
+
"type": "tap|type|scroll|swipe|wait|assert|screenshot",
|
|
260
|
+
"selector": {
|
|
261
|
+
"strategy": "text|id|accessibility-id|xpath|class",
|
|
262
|
+
"value": "selector_value",
|
|
263
|
+
"index": 0
|
|
264
|
+
},
|
|
265
|
+
"value": "text_to_type (only for type action)",
|
|
266
|
+
"direction": "up|down|left|right (only for scroll/swipe)",
|
|
267
|
+
"duration": 1000,
|
|
268
|
+
"assertionType": "exists|visible|text|enabled (only for assert)",
|
|
269
|
+
"expectedValue": "expected_value (only for assert)"
|
|
270
|
+
}
|
|
271
|
+
],
|
|
272
|
+
"reasoning": "Brief explanation of the action sequence and why these selectors were chosen"
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
**Critical Rules:**
|
|
276
|
+
1. Return ONLY valid JSON, no markdown code blocks
|
|
277
|
+
2. Always prefer text-based selectors over XPath
|
|
278
|
+
3. Use accessibility-id or resource-id when available
|
|
279
|
+
4. Include reasoning for your selector choices
|
|
280
|
+
5. If no action is possible, return empty actions array with explanation
|
|
281
|
+
6. Keep action sequences minimal (1-3 actions preferred)
|
|
282
|
+
7. Add wait actions if UI transitions are expected
|
|
283
|
+
|
|
284
|
+
Generate the JSON action plan now:`;
|
|
285
|
+
}
|
|
286
|
+
/**
|
|
287
|
+
* Truncate text to max length
|
|
288
|
+
*/
|
|
289
|
+
truncateText(text, maxLength) {
|
|
290
|
+
if (text.length <= maxLength) {
|
|
291
|
+
return text;
|
|
292
|
+
}
|
|
293
|
+
return text.substring(0, maxLength - 3) + '...';
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Estimate token count (rough approximation)
|
|
297
|
+
*/
|
|
298
|
+
estimateTokens(text) {
|
|
299
|
+
// Rough estimate: 1 token ≈ 4 characters
|
|
300
|
+
return Math.ceil(text.length / 4);
|
|
301
|
+
}
|
|
302
|
+
/**
|
|
303
|
+
* Build a few-shot learning prompt with examples
|
|
304
|
+
*/
|
|
305
|
+
buildFewShotPrompt(stepText, uiContext, examples) {
|
|
306
|
+
const sections = [];
|
|
307
|
+
sections.push(llm_config_1.PromptTemplates.actionPlanning);
|
|
308
|
+
sections.push('');
|
|
309
|
+
sections.push('**Example Scenarios:**');
|
|
310
|
+
sections.push('');
|
|
311
|
+
examples.forEach((example, index) => {
|
|
312
|
+
sections.push(`Example ${index + 1}:`);
|
|
313
|
+
sections.push(`Step: "${example.step}"`);
|
|
314
|
+
if (example.context) {
|
|
315
|
+
sections.push(`Context: ${example.context}`);
|
|
316
|
+
}
|
|
317
|
+
sections.push(`Actions: ${JSON.stringify(example.actions, null, 2)}`);
|
|
318
|
+
sections.push('');
|
|
319
|
+
});
|
|
320
|
+
sections.push('**Now, solve this:**');
|
|
321
|
+
sections.push('');
|
|
322
|
+
sections.push('**Current UI State:**');
|
|
323
|
+
sections.push(this.formatUIContext(uiContext));
|
|
324
|
+
sections.push('');
|
|
325
|
+
sections.push(`**Test Step:** "${stepText}"`);
|
|
326
|
+
sections.push('');
|
|
327
|
+
sections.push(this.getOutputFormatInstructions());
|
|
328
|
+
return sections.join('\n');
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
exports.PromptBuilder = PromptBuilder;
|
|
332
|
+
exports.default = PromptBuilder;
|
|
333
|
+
//# sourceMappingURL=promptBuilder.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"promptBuilder.js","sourceRoot":"","sources":["../../src/utils/promptBuilder.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Technician App Knowledge Base
|
|
2
|
+
|
|
3
|
+
## App Overview
|
|
4
|
+
A mobile application for technicians to manage jobs, check-in/checkout, and capture photos for completed work.
|
|
5
|
+
|
|
6
|
+
## Common Screens and Elements
|
|
7
|
+
|
|
8
|
+
### Get Started Screen
|
|
9
|
+
- Get Started buton: text="Get Started"
|
|
10
|
+
|
|
11
|
+
### Login Screen
|
|
12
|
+
- Mobile number input field: accessibility-id="mobile-number-input" or text contains "mobile number"
|
|
13
|
+
- Continue/Next button: text="Continue" or "Next"
|
|
14
|
+
- OTP input fields: Usually 4-6 digit fields
|
|
15
|
+
|
|
16
|
+
### OTP Screen
|
|
17
|
+
- OTP input boxes: accessibility-id contains "otp" or "code"
|
|
18
|
+
- Verify/Submit button: text="Verify" or "Submit"
|
|
19
|
+
- Resend OTP link: text contains "Resend"
|
|
20
|
+
|
|
21
|
+
### Permission Dialogs
|
|
22
|
+
- Notification permission:
|
|
23
|
+
- Android: text="Allow"
|
|
24
|
+
- iOS: text="Allow" or "OK"
|
|
25
|
+
- Location permission:
|
|
26
|
+
- Android: text="Allow" or "While using the app"
|
|
27
|
+
- iOS: text="Allow While Using App" or "Allow Once"
|
|
28
|
+
- Camera permission:
|
|
29
|
+
- Android: text="Allow" or "Only this time"
|
|
30
|
+
- iOS: text="OK" or "Allow"
|
|
31
|
+
- Photo library:
|
|
32
|
+
- text="Allow access to all photos" or "Select Photos"
|
|
33
|
+
|
|
34
|
+
### Home Screen
|
|
35
|
+
- Bottom navigation tabs:
|
|
36
|
+
- Jobs: accessibility-id="tab-jobs" or text="My Jobs"
|
|
37
|
+
- Opportunities(optional): accessibility-id="tab-opportunities" or text="Opportunities"
|
|
38
|
+
- Estimates(optional): accessibility-id="tab-opportunities" or text="Estimates"
|
|
39
|
+
- Profile: accessibility-id="tab-profile" or text="Profile"
|
|
40
|
+
|
|
41
|
+
### Jobs Screen
|
|
42
|
+
- Search bar: accessibility-id="job-search" or hint="Search jobs"
|
|
43
|
+
- Job list items: class="RecyclerView" (Android) or "UITableView" (iOS)
|
|
44
|
+
- Each job card contains:
|
|
45
|
+
- Job title: accessibility-id contains "job-title"
|
|
46
|
+
- Job location: accessibility-id contains "job-location"
|
|
47
|
+
- Job status badge
|
|
48
|
+
|
|
49
|
+
### Job Details Screen
|
|
50
|
+
- Job title header
|
|
51
|
+
- Job description section
|
|
52
|
+
- Check-in button: text="Check In" or "Start Job"
|
|
53
|
+
- Check-out button: text="Check Out" or "Complete Job"
|
|
54
|
+
- Take photo button: text="Take Photo" or "Add Photo"
|
|
55
|
+
- Navigation back button: usually top-left
|
|
56
|
+
|
|
57
|
+
### Check-in/Checkout Screens
|
|
58
|
+
- Location status indicator
|
|
59
|
+
- Photo capture area
|
|
60
|
+
- Notes/Comments field: hint contains "notes" or "comments"
|
|
61
|
+
- Submit button: text="Submit" or "Confirm"
|
|
62
|
+
|
|
63
|
+
### Camera Screen
|
|
64
|
+
- Capture button: Large circular button at bottom center
|
|
65
|
+
- Gallery/Library button: Usually bottom-left
|
|
66
|
+
- Flash toggle: Usually top portion
|
|
67
|
+
- Confirm/Use photo button: text="Use Photo" or checkmark icon
|
|
68
|
+
|
|
69
|
+
## Common Patterns
|
|
70
|
+
|
|
71
|
+
### Navigation
|
|
72
|
+
- Bottom tab navigation for main sections
|
|
73
|
+
- Back button in top-left for sub-screens
|
|
74
|
+
- Hamburger menu (if present) in top-right for settings
|
|
75
|
+
|
|
76
|
+
### Text Input
|
|
77
|
+
- Always look for hint text or placeholder text
|
|
78
|
+
- Input fields often have resource-id ending in "input" or "edit"
|
|
79
|
+
|
|
80
|
+
### Buttons
|
|
81
|
+
- Primary actions: Usually prominent colors (blue, green)
|
|
82
|
+
- Text often includes: "Continue", "Submit", "Confirm", "Next", "Done"
|
|
83
|
+
- Cancel/Back: Usually gray or outline style
|
|
84
|
+
|
|
85
|
+
### Lists and Scrolling
|
|
86
|
+
- Job lists and opportunity lists are scrollable
|
|
87
|
+
- Use scroll gesture with direction "down" to load more
|
|
88
|
+
- Pull to refresh at top of lists
|
|
89
|
+
|
|
90
|
+
## Error Handling
|
|
91
|
+
- Toast messages appear at bottom (Android) or top (iOS)
|
|
92
|
+
- Error dialogs usually have "OK" or "Dismiss" button
|
|
93
|
+
- Network errors may show retry button
|
|
94
|
+
|
|
95
|
+
## Important Notes
|
|
96
|
+
- Always wait after location permission grant (GPS initialization takes time)
|
|
97
|
+
- Photos may take 2-3 seconds to process after capture
|
|
98
|
+
- Job status updates may have slight delay (1-2 seconds)
|
|
99
|
+
- Prefer text-based selectors over XPath for reliability
|
|
100
|
+
- Use accessibility-id when available for best cross-platform support
|