@maccesar/titools 2.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/AGENTS-TEMPLATE.md +173 -0
- package/README.md +867 -0
- package/agents/ti-researcher.md +108 -0
- package/bin/titools.js +53 -0
- package/lib/commands/agents.js +126 -0
- package/lib/commands/install.js +188 -0
- package/lib/commands/uninstall.js +215 -0
- package/lib/commands/update.js +159 -0
- package/lib/config.js +119 -0
- package/lib/downloader.js +153 -0
- package/lib/installer.js +253 -0
- package/lib/platform.js +108 -0
- package/lib/symlink.js +142 -0
- package/lib/utils.js +270 -0
- package/package.json +67 -0
- package/skills/alloy-expert/SKILL.md +247 -0
- package/skills/alloy-expert/assets/ControllerAutoCleanup.js +182 -0
- package/skills/alloy-expert/references/alloy-structure.md +381 -0
- package/skills/alloy-expert/references/anti-patterns.md +133 -0
- package/skills/alloy-expert/references/code-conventions.md +469 -0
- package/skills/alloy-expert/references/contracts.md +280 -0
- package/skills/alloy-expert/references/controller-patterns.md +520 -0
- package/skills/alloy-expert/references/error-handling.md +484 -0
- package/skills/alloy-expert/references/examples.md +735 -0
- package/skills/alloy-expert/references/migration-patterns.md +298 -0
- package/skills/alloy-expert/references/patterns.md +448 -0
- package/skills/alloy-expert/references/performance-patterns.md +855 -0
- package/skills/alloy-expert/references/security-patterns.md +847 -0
- package/skills/alloy-expert/references/state-management.md +779 -0
- package/skills/alloy-expert/references/testing.md +872 -0
- package/skills/alloy-guides/SKILL.md +214 -0
- package/skills/alloy-guides/references/CLI_TASKS.md +243 -0
- package/skills/alloy-guides/references/CONCEPTS.md +191 -0
- package/skills/alloy-guides/references/CONTROLLERS.md +298 -0
- package/skills/alloy-guides/references/MODELS.md +1028 -0
- package/skills/alloy-guides/references/PURGETSS.md +56 -0
- package/skills/alloy-guides/references/VIEWS_DYNAMIC.md +242 -0
- package/skills/alloy-guides/references/VIEWS_STYLES.md +388 -0
- package/skills/alloy-guides/references/VIEWS_WITHOUT_CONTROLLERS.md +109 -0
- package/skills/alloy-guides/references/VIEWS_XML.md +558 -0
- package/skills/alloy-guides/references/WIDGETS.md +176 -0
- package/skills/alloy-howtos/SKILL.md +203 -0
- package/skills/alloy-howtos/references/best_practices.md +138 -0
- package/skills/alloy-howtos/references/cli_reference.md +253 -0
- package/skills/alloy-howtos/references/config_files.md +87 -0
- package/skills/alloy-howtos/references/custom_tags.md +147 -0
- package/skills/alloy-howtos/references/debugging_troubleshooting.md +101 -0
- package/skills/alloy-howtos/references/samples.md +167 -0
- package/skills/purgetss/SKILL.md +442 -0
- package/skills/purgetss/assets/purgetss.config.cjs +17 -0
- package/skills/purgetss/references/EXAMPLES.md +247 -0
- package/skills/purgetss/references/animation-system.md +1294 -0
- package/skills/purgetss/references/apply-directive.md +375 -0
- package/skills/purgetss/references/arbitrary-values.md +612 -0
- package/skills/purgetss/references/class-index.md +1350 -0
- package/skills/purgetss/references/cli-commands.md +948 -0
- package/skills/purgetss/references/configurable-properties.md +654 -0
- package/skills/purgetss/references/custom-rules.md +161 -0
- package/skills/purgetss/references/customization-deep-dive.md +722 -0
- package/skills/purgetss/references/dynamic-component-creation.md +489 -0
- package/skills/purgetss/references/grid-layout.md +455 -0
- package/skills/purgetss/references/icon-fonts.md +609 -0
- package/skills/purgetss/references/installation-setup.md +366 -0
- package/skills/purgetss/references/opacity-modifier.md +291 -0
- package/skills/purgetss/references/platform-modifiers.md +479 -0
- package/skills/purgetss/references/smart-mappings.md +42 -0
- package/skills/purgetss/references/titanium-resets.md +359 -0
- package/skills/purgetss/references/ui-ux-design.md +1526 -0
- package/skills/ti-guides/SKILL.md +94 -0
- package/skills/ti-guides/references/advanced-data-and-images.md +19 -0
- package/skills/ti-guides/references/alloy-cli-advanced.md +84 -0
- package/skills/ti-guides/references/alloy-data-mastery.md +29 -0
- package/skills/ti-guides/references/alloy-widgets-and-themes.md +19 -0
- package/skills/ti-guides/references/android-manifest.md +97 -0
- package/skills/ti-guides/references/app-distribution.md +258 -0
- package/skills/ti-guides/references/application-frameworks.md +377 -0
- package/skills/ti-guides/references/cli-reference.md +402 -0
- package/skills/ti-guides/references/coding-best-practices.md +102 -0
- package/skills/ti-guides/references/commonjs-advanced.md +134 -0
- package/skills/ti-guides/references/hello-world.md +100 -0
- package/skills/ti-guides/references/hyperloop-native-access.md +62 -0
- package/skills/ti-guides/references/javascript-primer.md +411 -0
- package/skills/ti-guides/references/reserved-words.md +36 -0
- package/skills/ti-guides/references/resources.md +183 -0
- package/skills/ti-guides/references/style-and-conventions.md +48 -0
- package/skills/ti-guides/references/tiapp-config.md +609 -0
- package/skills/ti-howtos/SKILL.md +174 -0
- package/skills/ti-howtos/references/android-platform-deep-dives.md +658 -0
- package/skills/ti-howtos/references/automation-fastlane-appium.md +95 -0
- package/skills/ti-howtos/references/buffer-codec-streams.md +140 -0
- package/skills/ti-howtos/references/cross-platform-development.md +348 -0
- package/skills/ti-howtos/references/debugging-profiling.md +543 -0
- package/skills/ti-howtos/references/extending-titanium.md +723 -0
- package/skills/ti-howtos/references/google-maps-v2.md +169 -0
- package/skills/ti-howtos/references/ios-map-kit.md +143 -0
- package/skills/ti-howtos/references/ios-platform-deep-dives.md +783 -0
- package/skills/ti-howtos/references/local-data-sources.md +301 -0
- package/skills/ti-howtos/references/location-and-maps.md +252 -0
- package/skills/ti-howtos/references/media-apis.md +210 -0
- package/skills/ti-howtos/references/notification-services.md +599 -0
- package/skills/ti-howtos/references/remote-data-sources.md +349 -0
- package/skills/ti-howtos/references/tutorials.md +502 -0
- package/skills/ti-howtos/references/using-modules.md +237 -0
- package/skills/ti-howtos/references/web-content-integration.md +307 -0
- package/skills/ti-howtos/references/webpack-build-pipeline.md +78 -0
- package/skills/ti-ui/SKILL.md +179 -0
- package/skills/ti-ui/references/accessibility-deep-dive.md +242 -0
- package/skills/ti-ui/references/animation-and-matrices.md +599 -0
- package/skills/ti-ui/references/application-structures.md +655 -0
- package/skills/ti-ui/references/custom-fonts-styling.md +579 -0
- package/skills/ti-ui/references/event-handling.md +393 -0
- package/skills/ti-ui/references/gestures.md +473 -0
- package/skills/ti-ui/references/icons-and-splash-screens.md +409 -0
- package/skills/ti-ui/references/layouts-and-positioning.md +462 -0
- package/skills/ti-ui/references/listviews-and-performance.md +619 -0
- package/skills/ti-ui/references/orientation.md +362 -0
- package/skills/ti-ui/references/platform-ui-android.md +635 -0
- package/skills/ti-ui/references/platform-ui-ios.md +469 -0
- package/skills/ti-ui/references/scrolling-views.md +252 -0
- package/skills/ti-ui/references/tableviews.md +568 -0
|
@@ -0,0 +1,872 @@
|
|
|
1
|
+
# Testing Guide for Titanium + Alloy
|
|
2
|
+
|
|
3
|
+
:::info OPTIONAL - Advanced Topic
|
|
4
|
+
**This guide covers automated testing, which is OPTIONAL for Titanium/Alloy projects.**
|
|
5
|
+
|
|
6
|
+
Testing is useful for:
|
|
7
|
+
- Teams practicing CI/CD
|
|
8
|
+
- Projects with complex business logic
|
|
9
|
+
- Refactoring confidence
|
|
10
|
+
|
|
11
|
+
If you prefer manual testing on device, you can safely skip this guide.
|
|
12
|
+
:::
|
|
13
|
+
|
|
14
|
+
## What CAN Be Tested (Without Compiling)
|
|
15
|
+
|
|
16
|
+
✅ **Pure JavaScript Logic:**
|
|
17
|
+
- Services (authService, navigationService, etc.)
|
|
18
|
+
- Helpers/Utils (formatters, validators, parsers)
|
|
19
|
+
- Business logic (calculations, data transformations)
|
|
20
|
+
- Models (with mocked database)
|
|
21
|
+
|
|
22
|
+
## What CANNOT Be Easily Tested
|
|
23
|
+
|
|
24
|
+
❌ **Requires Compiled App:**
|
|
25
|
+
- Controllers (UI interactions)
|
|
26
|
+
- Views/XML (Titanium UI components)
|
|
27
|
+
- Native modules (device-specific features)
|
|
28
|
+
- End-to-end user flows (use Appium for this)
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Testing Philosophy
|
|
33
|
+
|
|
34
|
+
**Test behavior, not implementation.** Focus on what the code does from the outside, not how it achieves it internally.
|
|
35
|
+
|
|
36
|
+
## Project Structure
|
|
37
|
+
|
|
38
|
+
```
|
|
39
|
+
app/
|
|
40
|
+
├── specs/ # Test files
|
|
41
|
+
│ ├── unit/ # Unit tests
|
|
42
|
+
│ │ ├── services/ # Service layer tests
|
|
43
|
+
│ │ │ ├── authService.spec.js
|
|
44
|
+
│ │ │ └── tokenStorage.spec.js
|
|
45
|
+
│ │ ├── helpers/ # Helper function tests
|
|
46
|
+
│ │ │ └── i18n.spec.js
|
|
47
|
+
│ │ ├── utils/
|
|
48
|
+
│ │ │ └── validator.spec.js
|
|
49
|
+
│ │ └── models/ # Model tests (if using SQLite)
|
|
50
|
+
│ └── integration/ # Integration tests
|
|
51
|
+
│ ├── controllers/ # Controller flow tests
|
|
52
|
+
│ │ └── login.spec.js
|
|
53
|
+
│ └── api/ # API integration tests
|
|
54
|
+
├── lib/
|
|
55
|
+
│ ├── testing/ # Test utilities
|
|
56
|
+
│ │ ├── mocks.js # Mock factories
|
|
57
|
+
│ │ ├── helpers.js # Test helper functions
|
|
58
|
+
│ │ └── setup.js # Test environment setup
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Writing Testable Code
|
|
62
|
+
|
|
63
|
+
### Dependency Injection for Testability
|
|
64
|
+
|
|
65
|
+
```javascript
|
|
66
|
+
// BAD: Hard to test - direct dependency
|
|
67
|
+
exports.getUser = async function getUser(id) {
|
|
68
|
+
const response = await api.get(`/users/${id}`)
|
|
69
|
+
return response.data
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// GOOD: Testable - injectable dependency
|
|
73
|
+
exports.getUser = async function getUser(id, apiClient = defaultApiClient) {
|
|
74
|
+
const response = await apiClient.get(`/users/${id}`)
|
|
75
|
+
return response.data
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Test with mock
|
|
79
|
+
describe('getUser', () => {
|
|
80
|
+
it('should fetch user from API', async () => {
|
|
81
|
+
const mockApi = {
|
|
82
|
+
get: jasmine.createSpy('get').and.resolveTo({
|
|
83
|
+
data: { id: 1, name: 'Test User' }
|
|
84
|
+
})
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const user = await getUser(1, mockApi)
|
|
88
|
+
|
|
89
|
+
expect(mockApi.get).toHaveBeenCalledWith('/users/1')
|
|
90
|
+
expect(user.name).toBe('Test User')
|
|
91
|
+
})
|
|
92
|
+
})
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## Unit Testing Services
|
|
96
|
+
|
|
97
|
+
```javascript
|
|
98
|
+
// specs/unit/services/authService.spec.js
|
|
99
|
+
const { login, logout } = require('lib/services/authService')
|
|
100
|
+
|
|
101
|
+
describe('AuthService', () => {
|
|
102
|
+
let mockApi
|
|
103
|
+
let mockTokenStorage
|
|
104
|
+
|
|
105
|
+
beforeEach(() => {
|
|
106
|
+
mockApi = {
|
|
107
|
+
post: jasmine.createSpy('post')
|
|
108
|
+
}
|
|
109
|
+
mockTokenStorage = {
|
|
110
|
+
save: jasmine.createSpy('save'),
|
|
111
|
+
get: jasmine.createSpy('get'),
|
|
112
|
+
clear: jasmine.createSpy('clear')
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
jasmine.clock().install()
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
afterEach(() => {
|
|
119
|
+
jasmine.clock().uninstall()
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
describe('login', () => {
|
|
123
|
+
it('should save token on successful login', async () => {
|
|
124
|
+
mockApi.post.and.resolveTo({
|
|
125
|
+
token: 'abc123',
|
|
126
|
+
user: { id: 1, name: 'Test User' }
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
const result = await login('user@test.com', 'password', mockApi, mockTokenStorage)
|
|
130
|
+
|
|
131
|
+
expect(mockTokenStorage.save).toHaveBeenCalledWith('abc123')
|
|
132
|
+
expect(result).toEqual({ id: 1, name: 'Test User' })
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('should throw on network error', async () => {
|
|
136
|
+
mockApi.post.and.rejectWith(new Error('Network error'))
|
|
137
|
+
|
|
138
|
+
await expectAsync(
|
|
139
|
+
login('user@test.com', 'password', mockApi, mockTokenStorage)
|
|
140
|
+
).toBeRejectedWith('Network error')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
it('should throw on invalid credentials', async () => {
|
|
144
|
+
const error = new Error()
|
|
145
|
+
error.status = 401
|
|
146
|
+
mockApi.post.and.rejectWith(error)
|
|
147
|
+
|
|
148
|
+
await expectAsync(
|
|
149
|
+
login('user@test.com', 'wrong', mockApi, mockTokenStorage)
|
|
150
|
+
).toBeRejected()
|
|
151
|
+
})
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Unit Testing Helpers
|
|
157
|
+
|
|
158
|
+
```javascript
|
|
159
|
+
// specs/unit/helpers/i18n.spec.js
|
|
160
|
+
const { getPluralMessages } = require('lib/helpers/i18n')
|
|
161
|
+
|
|
162
|
+
describe('i18n Helper', () => {
|
|
163
|
+
beforeEach(() => {
|
|
164
|
+
// Mock L() function
|
|
165
|
+
global.L = jasmine.createSpy('L').and.callFake((key) => {
|
|
166
|
+
const strings = {
|
|
167
|
+
'one_message': 'You have 1 message',
|
|
168
|
+
'many_messages': 'You have %d messages'
|
|
169
|
+
}
|
|
170
|
+
return strings[key] || key
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
describe('getPluralMessages', () => {
|
|
175
|
+
it('should return singular for count = 1', () => {
|
|
176
|
+
const result = getPluralMessages(1)
|
|
177
|
+
expect(result).toBe('You have 1 message')
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
it('should return plural for count > 1', () => {
|
|
181
|
+
const result = getPluralMessages(5)
|
|
182
|
+
expect(result).toBe('You have 5 messages')
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it('should handle zero messages', () => {
|
|
186
|
+
const result = getPluralMessages(0)
|
|
187
|
+
expect(result).toBe('You have 0 messages')
|
|
188
|
+
})
|
|
189
|
+
})
|
|
190
|
+
})
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
## Integration Testing Controllers
|
|
194
|
+
|
|
195
|
+
```javascript
|
|
196
|
+
// specs/integration/controllers/login.spec.js
|
|
197
|
+
describe('Login Controller', () => {
|
|
198
|
+
let controller
|
|
199
|
+
let mockAuthService
|
|
200
|
+
|
|
201
|
+
beforeEach(() => {
|
|
202
|
+
mockAuthService = {
|
|
203
|
+
login: jasmine.createSpy('login')
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
controller = Alloy.createController('login')
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
afterEach(() => {
|
|
210
|
+
// Always cleanup
|
|
211
|
+
if (controller.cleanup) {
|
|
212
|
+
controller.cleanup()
|
|
213
|
+
}
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
describe('doLogin', () => {
|
|
217
|
+
it('should call authService with form data', async () => {
|
|
218
|
+
controller.emailTextField.value = 'user@test.com'
|
|
219
|
+
controller.passwordTextField.value = 'password123'
|
|
220
|
+
|
|
221
|
+
mockAuthService.login.and.resolveTo({ id: 1, name: 'User' })
|
|
222
|
+
|
|
223
|
+
await controller.doLogin()
|
|
224
|
+
|
|
225
|
+
expect(mockAuthService.login).toHaveBeenCalledWith(
|
|
226
|
+
'user@test.com',
|
|
227
|
+
'password123'
|
|
228
|
+
)
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
it('should show error on login failure', async () => {
|
|
232
|
+
controller.emailTextField.value = 'user@test.com'
|
|
233
|
+
controller.passwordTextField.value = 'wrong'
|
|
234
|
+
|
|
235
|
+
mockAuthService.login.and.rejectWith(new Error('Invalid credentials'))
|
|
236
|
+
|
|
237
|
+
await controller.doLogin()
|
|
238
|
+
|
|
239
|
+
expect(controller.errorLabel.text).toBe('Invalid credentials')
|
|
240
|
+
expect(controller.errorLabel.visible).toBe(true)
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
it('should disable button during login', async () => {
|
|
244
|
+
const button = controller.loginButton
|
|
245
|
+
|
|
246
|
+
mockAuthService.login.and.callFake(() => {
|
|
247
|
+
expect(button.enabled).toBe(false)
|
|
248
|
+
return Promise.resolve({ id: 1 })
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
await controller.doLogin()
|
|
252
|
+
|
|
253
|
+
expect(button.enabled).toBe(true)
|
|
254
|
+
})
|
|
255
|
+
})
|
|
256
|
+
|
|
257
|
+
describe('cleanup', () => {
|
|
258
|
+
it('should remove event listeners', () => {
|
|
259
|
+
const spy = spyOn(Ti.App, 'removeEventListener')
|
|
260
|
+
|
|
261
|
+
controller.cleanup()
|
|
262
|
+
|
|
263
|
+
expect(controller._isCleanedUp).toBe(true)
|
|
264
|
+
})
|
|
265
|
+
|
|
266
|
+
it('should destroy Alloy bindings', () => {
|
|
267
|
+
const destroySpy = spyOn(controller, '$').and.returnValue({
|
|
268
|
+
destroy: jasmine.createSpy('destroy')
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
controller.cleanup()
|
|
272
|
+
|
|
273
|
+
expect(controller.$.destroy).toHaveBeenCalled()
|
|
274
|
+
})
|
|
275
|
+
})
|
|
276
|
+
})
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
## Mock Factory
|
|
280
|
+
|
|
281
|
+
```javascript
|
|
282
|
+
// lib/testing/mocks.js
|
|
283
|
+
exports.Mocks = {
|
|
284
|
+
createHTTPClient() {
|
|
285
|
+
return {
|
|
286
|
+
open: jasmine.createSpy('open'),
|
|
287
|
+
send: jasmine.createSpy('send'),
|
|
288
|
+
setRequestHeader: jasmine.createSpy('setRequestHeader'),
|
|
289
|
+
onload: null,
|
|
290
|
+
onerror: null,
|
|
291
|
+
status: null,
|
|
292
|
+
responseText: null
|
|
293
|
+
}
|
|
294
|
+
},
|
|
295
|
+
|
|
296
|
+
createTokenStorage() {
|
|
297
|
+
return {
|
|
298
|
+
save: jasmine.createSpy('save'),
|
|
299
|
+
get: jasmine.createSpy('get'),
|
|
300
|
+
clear: jasmine.createSpy('clear')
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
|
|
304
|
+
createLogger() {
|
|
305
|
+
return {
|
|
306
|
+
debug: jasmine.createSpy('debug'),
|
|
307
|
+
info: jasmine.createSpy('info'),
|
|
308
|
+
warn: jasmine.createSpy('warn'),
|
|
309
|
+
error: jasmine.createSpy('error')
|
|
310
|
+
}
|
|
311
|
+
},
|
|
312
|
+
|
|
313
|
+
createView() {
|
|
314
|
+
return {
|
|
315
|
+
addEventListener: jasmine.createSpy('addEventListener'),
|
|
316
|
+
removeEventListener: jasmine.createSpy('removeEventListener'),
|
|
317
|
+
applyProperties: jasmine.createSpy('applyProperties'),
|
|
318
|
+
visible: true,
|
|
319
|
+
enabled: true,
|
|
320
|
+
text: '',
|
|
321
|
+
value: ''
|
|
322
|
+
}
|
|
323
|
+
},
|
|
324
|
+
|
|
325
|
+
createNetworkMock() {
|
|
326
|
+
return {
|
|
327
|
+
addEventListener: jasmine.createSpy('addEventListener'),
|
|
328
|
+
removeEventListener: jasmine.createSpy('removeEventListener'),
|
|
329
|
+
online: true
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
## Test Helper Functions
|
|
336
|
+
|
|
337
|
+
```javascript
|
|
338
|
+
// lib/testing/helpers.js
|
|
339
|
+
exports.TestHelpers = {
|
|
340
|
+
// Wait for async operations
|
|
341
|
+
async waitFor(condition, timeout = 1000) {
|
|
342
|
+
const start = Date.now()
|
|
343
|
+
|
|
344
|
+
while (Date.now() - start < timeout) {
|
|
345
|
+
if (await condition()) {
|
|
346
|
+
return true
|
|
347
|
+
}
|
|
348
|
+
await new Promise(resolve => setTimeout(resolve, 10))
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
throw new Error('Condition not met within timeout')
|
|
352
|
+
},
|
|
353
|
+
|
|
354
|
+
// Trigger Alloy event
|
|
355
|
+
triggerEvent(controller, eventName, data) {
|
|
356
|
+
const listeners = controller._eventListeners?.[eventName] || []
|
|
357
|
+
|
|
358
|
+
listeners.forEach(listener => {
|
|
359
|
+
listener(data)
|
|
360
|
+
})
|
|
361
|
+
},
|
|
362
|
+
|
|
363
|
+
// Mock Ti.Platform properties
|
|
364
|
+
mockPlatform(properties) {
|
|
365
|
+
const original = {}
|
|
366
|
+
|
|
367
|
+
Object.keys(properties).forEach(key => {
|
|
368
|
+
original[key] = Ti.Platform[key]
|
|
369
|
+
Ti.Platform[key] = properties[key]
|
|
370
|
+
})
|
|
371
|
+
|
|
372
|
+
return () => {
|
|
373
|
+
// Restore function
|
|
374
|
+
Object.keys(original).forEach(key => {
|
|
375
|
+
Ti.Platform[key] = original[key]
|
|
376
|
+
})
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
```
|
|
381
|
+
|
|
382
|
+
## Test Setup Configuration
|
|
383
|
+
|
|
384
|
+
```javascript
|
|
385
|
+
// lib/testing/setup.js
|
|
386
|
+
beforeAll(() => {
|
|
387
|
+
// Disable Alloy auto-cleanup for testing
|
|
388
|
+
Alloy.Globals.testMode = true
|
|
389
|
+
|
|
390
|
+
// Mock Ti.Platform for deterministic tests
|
|
391
|
+
spyOn(Ti.Platform, 'displayCaps').and.returnValue({
|
|
392
|
+
platformWidth: 375,
|
|
393
|
+
platformHeight: 667
|
|
394
|
+
})
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
afterAll(() => {
|
|
398
|
+
// Cleanup test resources
|
|
399
|
+
if (Alloy.Globals.testMode) {
|
|
400
|
+
Ti.App.Properties.removeAllProperties()
|
|
401
|
+
}
|
|
402
|
+
})
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
## Running Tests
|
|
406
|
+
|
|
407
|
+
```bash
|
|
408
|
+
# Run specific test suite
|
|
409
|
+
titanium test --platform android
|
|
410
|
+
|
|
411
|
+
# Run with coverage
|
|
412
|
+
titanium test --coverage
|
|
413
|
+
|
|
414
|
+
# Run specific file
|
|
415
|
+
titanium test --specs app/specs/unit/services/authService.spec.js
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
## Anti-Patterns to Avoid
|
|
419
|
+
|
|
420
|
+
| Anti-Pattern | Problem | Solution |
|
|
421
|
+
| -------------------------- | ------------------------------ | ------------------------------- |
|
|
422
|
+
| Testing private methods | Breaks on refactoring | Test public interface only |
|
|
423
|
+
| Mocking everything | Tests pass, code fails | Mock only external dependencies |
|
|
424
|
+
| No cleanup in tests | Memory leaks, interference | Always cleanup in afterEach |
|
|
425
|
+
| Testing XML/TSS | Fragile, implementation detail | Test rendered behavior instead |
|
|
426
|
+
| Hard dependencies | Untestable code | Use dependency injection |
|
|
427
|
+
| Shared state between tests | Flaky tests | Reset state in beforeEach |
|
|
428
|
+
|
|
429
|
+
## Test Checklist
|
|
430
|
+
|
|
431
|
+
Before committing code, verify:
|
|
432
|
+
|
|
433
|
+
- [ ] All unit tests pass
|
|
434
|
+
- [ ] New functionality has test coverage
|
|
435
|
+
- [ ] Controllers cleanup properly in tests
|
|
436
|
+
- [ ] Mocks are reset between tests
|
|
437
|
+
- [ ] No hard dependencies (use injection)
|
|
438
|
+
- [ ] Tests are independent (no shared state)
|
|
439
|
+
- [ ] Edge cases are covered (null, empty, error)
|
|
440
|
+
|
|
441
|
+
## End-to-End Testing with Appium
|
|
442
|
+
|
|
443
|
+
### Appium Setup
|
|
444
|
+
|
|
445
|
+
```bash
|
|
446
|
+
# Install Appium
|
|
447
|
+
npm install -g appium
|
|
448
|
+
|
|
449
|
+
# Install drivers
|
|
450
|
+
appium driver install xcuitest # iOS
|
|
451
|
+
appium driver install uiautomator2 # Android
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
### Appium Configuration
|
|
455
|
+
|
|
456
|
+
```javascript
|
|
457
|
+
// e2e/config/capabilities.js
|
|
458
|
+
exports.iOS = {
|
|
459
|
+
platformName: 'iOS',
|
|
460
|
+
'appium:automationName': 'XCUITest',
|
|
461
|
+
'appium:deviceName': 'iPhone 14',
|
|
462
|
+
'appium:platformVersion': '16.0',
|
|
463
|
+
'appium:app': '/path/to/your.app',
|
|
464
|
+
'appium:noReset': false
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
exports.android = {
|
|
468
|
+
platformName: 'Android',
|
|
469
|
+
'appium:automationName': 'UiAutomator2',
|
|
470
|
+
'appium:deviceName': 'Pixel 6',
|
|
471
|
+
'appium:platformVersion': '13',
|
|
472
|
+
'appium:app': '/path/to/your.apk',
|
|
473
|
+
'appium:noReset': false
|
|
474
|
+
}
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### WebdriverIO Integration
|
|
478
|
+
|
|
479
|
+
```javascript
|
|
480
|
+
// e2e/wdio.conf.js
|
|
481
|
+
exports.config = {
|
|
482
|
+
runner: 'local',
|
|
483
|
+
specs: ['./e2e/specs/**/*.spec.js'],
|
|
484
|
+
maxInstances: 1,
|
|
485
|
+
|
|
486
|
+
capabilities: [{
|
|
487
|
+
...require('./config/capabilities').iOS
|
|
488
|
+
}],
|
|
489
|
+
|
|
490
|
+
services: ['appium'],
|
|
491
|
+
appium: {
|
|
492
|
+
command: 'appium'
|
|
493
|
+
},
|
|
494
|
+
|
|
495
|
+
framework: 'mocha',
|
|
496
|
+
mochaOpts: {
|
|
497
|
+
timeout: 60000
|
|
498
|
+
},
|
|
499
|
+
|
|
500
|
+
reporters: ['spec'],
|
|
501
|
+
|
|
502
|
+
// Hooks
|
|
503
|
+
beforeSession: () => {
|
|
504
|
+
// Setup before each session
|
|
505
|
+
},
|
|
506
|
+
|
|
507
|
+
afterTest: async (test, context, { passed }) => {
|
|
508
|
+
if (!passed) {
|
|
509
|
+
await browser.saveScreenshot(`./e2e/screenshots/${test.title}.png`)
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
### E2E Test Example
|
|
516
|
+
|
|
517
|
+
```javascript
|
|
518
|
+
// e2e/specs/login.spec.js
|
|
519
|
+
describe('Login Flow', () => {
|
|
520
|
+
beforeEach(async () => {
|
|
521
|
+
// Ensure we're on login screen
|
|
522
|
+
await $('~loginScreen').waitForDisplayed({ timeout: 5000 })
|
|
523
|
+
})
|
|
524
|
+
|
|
525
|
+
it('should login with valid credentials', async () => {
|
|
526
|
+
// Enter email
|
|
527
|
+
const emailField = await $('~emailField')
|
|
528
|
+
await emailField.setValue('test@example.com')
|
|
529
|
+
|
|
530
|
+
// Enter password
|
|
531
|
+
const passwordField = await $('~passwordField')
|
|
532
|
+
await passwordField.setValue('password123')
|
|
533
|
+
|
|
534
|
+
// Tap login button
|
|
535
|
+
const loginButton = await $('~loginButton')
|
|
536
|
+
await loginButton.click()
|
|
537
|
+
|
|
538
|
+
// Wait for home screen
|
|
539
|
+
const homeScreen = await $('~homeScreen')
|
|
540
|
+
await homeScreen.waitForDisplayed({ timeout: 10000 })
|
|
541
|
+
|
|
542
|
+
// Verify we're logged in
|
|
543
|
+
const welcomeLabel = await $('~welcomeLabel')
|
|
544
|
+
const text = await welcomeLabel.getText()
|
|
545
|
+
expect(text).toContain('Welcome')
|
|
546
|
+
})
|
|
547
|
+
|
|
548
|
+
it('should show error for invalid credentials', async () => {
|
|
549
|
+
const emailField = await $('~emailField')
|
|
550
|
+
await emailField.setValue('wrong@example.com')
|
|
551
|
+
|
|
552
|
+
const passwordField = await $('~passwordField')
|
|
553
|
+
await passwordField.setValue('wrongpassword')
|
|
554
|
+
|
|
555
|
+
const loginButton = await $('~loginButton')
|
|
556
|
+
await loginButton.click()
|
|
557
|
+
|
|
558
|
+
// Wait for error message
|
|
559
|
+
const errorLabel = await $('~errorLabel')
|
|
560
|
+
await errorLabel.waitForDisplayed({ timeout: 5000 })
|
|
561
|
+
|
|
562
|
+
const errorText = await errorLabel.getText()
|
|
563
|
+
expect(errorText).toContain('Invalid')
|
|
564
|
+
})
|
|
565
|
+
})
|
|
566
|
+
```
|
|
567
|
+
|
|
568
|
+
### Adding Accessibility IDs for Testing
|
|
569
|
+
|
|
570
|
+
```xml
|
|
571
|
+
<!-- views/auth/login.xml -->
|
|
572
|
+
<Window testId="loginScreen">
|
|
573
|
+
<TextField id="emailField" testId="emailField" />
|
|
574
|
+
<TextField id="passwordField" testId="passwordField" />
|
|
575
|
+
<Button id="loginBtn" testId="loginButton" />
|
|
576
|
+
<Label id="errorLabel" testId="errorLabel" />
|
|
577
|
+
</Window>
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
```javascript
|
|
581
|
+
// In controller or alloy.js - map testId to accessibilityLabel
|
|
582
|
+
if (Alloy.CFG.debug) {
|
|
583
|
+
// Auto-set accessibilityLabel from testId during development
|
|
584
|
+
Ti.UI.defaultUnit = 'dp'
|
|
585
|
+
}
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
### Page Object Pattern
|
|
589
|
+
|
|
590
|
+
```javascript
|
|
591
|
+
// e2e/pages/LoginPage.js
|
|
592
|
+
class LoginPage {
|
|
593
|
+
get emailField() { return $('~emailField') }
|
|
594
|
+
get passwordField() { return $('~passwordField') }
|
|
595
|
+
get loginButton() { return $('~loginButton') }
|
|
596
|
+
get errorLabel() { return $('~errorLabel') }
|
|
597
|
+
|
|
598
|
+
async login(email, password) {
|
|
599
|
+
await this.emailField.setValue(email)
|
|
600
|
+
await this.passwordField.setValue(password)
|
|
601
|
+
await this.loginButton.click()
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
async waitForError() {
|
|
605
|
+
await this.errorLabel.waitForDisplayed({ timeout: 5000 })
|
|
606
|
+
return this.errorLabel.getText()
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
module.exports = new LoginPage()
|
|
611
|
+
```
|
|
612
|
+
|
|
613
|
+
```javascript
|
|
614
|
+
// e2e/specs/login.spec.js
|
|
615
|
+
const LoginPage = require('../pages/LoginPage')
|
|
616
|
+
const HomePage = require('../pages/HomePage')
|
|
617
|
+
|
|
618
|
+
describe('Login Flow', () => {
|
|
619
|
+
it('should login successfully', async () => {
|
|
620
|
+
await LoginPage.login('test@example.com', 'password123')
|
|
621
|
+
await HomePage.waitForDisplayed()
|
|
622
|
+
expect(await HomePage.isLoggedIn()).toBe(true)
|
|
623
|
+
})
|
|
624
|
+
})
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
## CI/CD Integration
|
|
628
|
+
|
|
629
|
+
### GitHub Actions Workflow
|
|
630
|
+
|
|
631
|
+
```yaml
|
|
632
|
+
# .github/workflows/ci.yml
|
|
633
|
+
name: CI
|
|
634
|
+
|
|
635
|
+
on:
|
|
636
|
+
push:
|
|
637
|
+
branches: [main, develop]
|
|
638
|
+
pull_request:
|
|
639
|
+
branches: [main]
|
|
640
|
+
|
|
641
|
+
jobs:
|
|
642
|
+
lint:
|
|
643
|
+
runs-on: ubuntu-latest
|
|
644
|
+
steps:
|
|
645
|
+
- uses: actions/checkout@v3
|
|
646
|
+
|
|
647
|
+
- name: Setup Node.js
|
|
648
|
+
uses: actions/setup-node@v3
|
|
649
|
+
with:
|
|
650
|
+
node-version: '18'
|
|
651
|
+
cache: 'npm'
|
|
652
|
+
|
|
653
|
+
- name: Install dependencies
|
|
654
|
+
run: npm ci
|
|
655
|
+
|
|
656
|
+
- name: Run ESLint
|
|
657
|
+
run: npm run lint
|
|
658
|
+
|
|
659
|
+
unit-tests:
|
|
660
|
+
runs-on: ubuntu-latest
|
|
661
|
+
needs: lint
|
|
662
|
+
steps:
|
|
663
|
+
- uses: actions/checkout@v3
|
|
664
|
+
|
|
665
|
+
- name: Setup Node.js
|
|
666
|
+
uses: actions/setup-node@v3
|
|
667
|
+
with:
|
|
668
|
+
node-version: '18'
|
|
669
|
+
|
|
670
|
+
- name: Install dependencies
|
|
671
|
+
run: npm ci
|
|
672
|
+
|
|
673
|
+
- name: Run unit tests
|
|
674
|
+
run: npm run test:unit
|
|
675
|
+
|
|
676
|
+
- name: Upload coverage
|
|
677
|
+
uses: codecov/codecov-action@v3
|
|
678
|
+
with:
|
|
679
|
+
files: ./coverage/lcov.info
|
|
680
|
+
|
|
681
|
+
build-ios:
|
|
682
|
+
runs-on: macos-latest
|
|
683
|
+
needs: unit-tests
|
|
684
|
+
steps:
|
|
685
|
+
- uses: actions/checkout@v3
|
|
686
|
+
|
|
687
|
+
- name: Setup Node.js
|
|
688
|
+
uses: actions/setup-node@v3
|
|
689
|
+
with:
|
|
690
|
+
node-version: '18'
|
|
691
|
+
|
|
692
|
+
- name: Install Titanium CLI
|
|
693
|
+
run: npm install -g titanium alloy
|
|
694
|
+
|
|
695
|
+
- name: Setup Titanium SDK
|
|
696
|
+
run: |
|
|
697
|
+
titanium sdk install latest
|
|
698
|
+
titanium sdk select latest
|
|
699
|
+
|
|
700
|
+
- name: Install dependencies
|
|
701
|
+
run: npm ci
|
|
702
|
+
|
|
703
|
+
- name: Build iOS
|
|
704
|
+
run: titanium build -p ios -T simulator -b
|
|
705
|
+
|
|
706
|
+
- name: Upload artifact
|
|
707
|
+
uses: actions/upload-artifact@v3
|
|
708
|
+
with:
|
|
709
|
+
name: ios-build
|
|
710
|
+
path: build/iphone/build/Products/Debug-iphonesimulator/*.app
|
|
711
|
+
|
|
712
|
+
build-android:
|
|
713
|
+
runs-on: ubuntu-latest
|
|
714
|
+
needs: unit-tests
|
|
715
|
+
steps:
|
|
716
|
+
- uses: actions/checkout@v3
|
|
717
|
+
|
|
718
|
+
- name: Setup JDK
|
|
719
|
+
uses: actions/setup-java@v3
|
|
720
|
+
with:
|
|
721
|
+
java-version: '11'
|
|
722
|
+
distribution: 'temurin'
|
|
723
|
+
|
|
724
|
+
- name: Setup Android SDK
|
|
725
|
+
uses: android-actions/setup-android@v2
|
|
726
|
+
|
|
727
|
+
- name: Setup Node.js
|
|
728
|
+
uses: actions/setup-node@v3
|
|
729
|
+
with:
|
|
730
|
+
node-version: '18'
|
|
731
|
+
|
|
732
|
+
- name: Install Titanium CLI
|
|
733
|
+
run: npm install -g titanium alloy
|
|
734
|
+
|
|
735
|
+
- name: Setup Titanium SDK
|
|
736
|
+
run: |
|
|
737
|
+
titanium sdk install latest
|
|
738
|
+
titanium sdk select latest
|
|
739
|
+
|
|
740
|
+
- name: Install dependencies
|
|
741
|
+
run: npm ci
|
|
742
|
+
|
|
743
|
+
- name: Build Android
|
|
744
|
+
run: titanium build -p android -T dist-playstore -K ${{ secrets.KEYSTORE_PATH }} -P ${{ secrets.KEYSTORE_PASSWORD }}
|
|
745
|
+
|
|
746
|
+
- name: Upload artifact
|
|
747
|
+
uses: actions/upload-artifact@v3
|
|
748
|
+
with:
|
|
749
|
+
name: android-build
|
|
750
|
+
path: dist/*.apk
|
|
751
|
+
|
|
752
|
+
e2e-tests:
|
|
753
|
+
runs-on: macos-latest
|
|
754
|
+
needs: [build-ios]
|
|
755
|
+
steps:
|
|
756
|
+
- uses: actions/checkout@v3
|
|
757
|
+
|
|
758
|
+
- name: Download iOS build
|
|
759
|
+
uses: actions/download-artifact@v3
|
|
760
|
+
with:
|
|
761
|
+
name: ios-build
|
|
762
|
+
path: ./build
|
|
763
|
+
|
|
764
|
+
- name: Setup Node.js
|
|
765
|
+
uses: actions/setup-node@v3
|
|
766
|
+
with:
|
|
767
|
+
node-version: '18'
|
|
768
|
+
|
|
769
|
+
- name: Install Appium
|
|
770
|
+
run: |
|
|
771
|
+
npm install -g appium
|
|
772
|
+
appium driver install xcuitest
|
|
773
|
+
|
|
774
|
+
- name: Install test dependencies
|
|
775
|
+
run: cd e2e && npm ci
|
|
776
|
+
|
|
777
|
+
- name: Start Appium
|
|
778
|
+
run: appium &
|
|
779
|
+
|
|
780
|
+
- name: Run E2E tests
|
|
781
|
+
run: cd e2e && npm run test
|
|
782
|
+
|
|
783
|
+
- name: Upload screenshots
|
|
784
|
+
if: failure()
|
|
785
|
+
uses: actions/upload-artifact@v3
|
|
786
|
+
with:
|
|
787
|
+
name: e2e-screenshots
|
|
788
|
+
path: e2e/screenshots/
|
|
789
|
+
```
|
|
790
|
+
|
|
791
|
+
### Fastlane Integration
|
|
792
|
+
|
|
793
|
+
```ruby
|
|
794
|
+
# fastlane/Fastfile
|
|
795
|
+
default_platform(:ios)
|
|
796
|
+
|
|
797
|
+
platform :ios do
|
|
798
|
+
desc "Build and upload to TestFlight"
|
|
799
|
+
lane :beta do
|
|
800
|
+
# Build with Titanium
|
|
801
|
+
sh "cd .. && titanium build -p ios -T dist-adhoc"
|
|
802
|
+
|
|
803
|
+
# Upload to TestFlight
|
|
804
|
+
upload_to_testflight(
|
|
805
|
+
ipa: "../dist/MyApp.ipa",
|
|
806
|
+
skip_waiting_for_build_processing: true
|
|
807
|
+
)
|
|
808
|
+
|
|
809
|
+
# Notify team
|
|
810
|
+
slack(
|
|
811
|
+
message: "New iOS beta available on TestFlight!",
|
|
812
|
+
channel: "#releases"
|
|
813
|
+
)
|
|
814
|
+
end
|
|
815
|
+
end
|
|
816
|
+
|
|
817
|
+
platform :android do
|
|
818
|
+
desc "Build and upload to Play Store"
|
|
819
|
+
lane :beta do
|
|
820
|
+
# Build with Titanium
|
|
821
|
+
sh "cd .. && titanium build -p android -T dist-playstore -K keystore.jks -P $KEYSTORE_PASSWORD"
|
|
822
|
+
|
|
823
|
+
# Upload to Play Store
|
|
824
|
+
upload_to_play_store(
|
|
825
|
+
track: 'internal',
|
|
826
|
+
aab: '../dist/MyApp.aab'
|
|
827
|
+
)
|
|
828
|
+
end
|
|
829
|
+
end
|
|
830
|
+
```
|
|
831
|
+
|
|
832
|
+
### Package.json Scripts
|
|
833
|
+
|
|
834
|
+
```json
|
|
835
|
+
{
|
|
836
|
+
"scripts": {
|
|
837
|
+
"lint": "eslint app/",
|
|
838
|
+
"test:unit": "titanium test --platform ios",
|
|
839
|
+
"test:e2e": "cd e2e && wdio run wdio.conf.js",
|
|
840
|
+
"test:e2e:android": "cd e2e && wdio run wdio.android.conf.js",
|
|
841
|
+
"build:ios": "titanium build -p ios -T device",
|
|
842
|
+
"build:android": "titanium build -p android -T device",
|
|
843
|
+
"build:ios:prod": "titanium build -p ios -T dist-appstore",
|
|
844
|
+
"build:android:prod": "titanium build -p android -T dist-playstore",
|
|
845
|
+
"deploy:ios": "cd fastlane && fastlane ios beta",
|
|
846
|
+
"deploy:android": "cd fastlane && fastlane android beta"
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
```
|
|
850
|
+
|
|
851
|
+
### Branch Protection Rules
|
|
852
|
+
|
|
853
|
+
Configure in GitHub repository settings:
|
|
854
|
+
|
|
855
|
+
| Rule | Setting |
|
|
856
|
+
| --------------------------------- | ------------------------------------------ |
|
|
857
|
+
| Require pull request reviews | 1 approval required |
|
|
858
|
+
| Require status checks | lint, unit-tests, build-ios, build-android |
|
|
859
|
+
| Require branches to be up to date | Yes |
|
|
860
|
+
| Include administrators | Yes |
|
|
861
|
+
|
|
862
|
+
## Testing Best Practices Summary
|
|
863
|
+
|
|
864
|
+
| Area | Practice |
|
|
865
|
+
| --------------------- | ------------------------------------------ |
|
|
866
|
+
| **Unit Tests** | Test business logic in services/helpers |
|
|
867
|
+
| **Integration Tests** | Test controller flows with mocked services |
|
|
868
|
+
| **E2E Tests** | Test critical user journeys |
|
|
869
|
+
| **Coverage** | Aim for 80%+ on services, 60%+ overall |
|
|
870
|
+
| **CI Pipeline** | Run lint -> unit tests -> build -> E2E |
|
|
871
|
+
| **Artifacts** | Save screenshots on failure |
|
|
872
|
+
| **Notifications** | Slack/email on build failures |
|