@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.
Files changed (120) hide show
  1. package/AGENTS-TEMPLATE.md +173 -0
  2. package/README.md +867 -0
  3. package/agents/ti-researcher.md +108 -0
  4. package/bin/titools.js +53 -0
  5. package/lib/commands/agents.js +126 -0
  6. package/lib/commands/install.js +188 -0
  7. package/lib/commands/uninstall.js +215 -0
  8. package/lib/commands/update.js +159 -0
  9. package/lib/config.js +119 -0
  10. package/lib/downloader.js +153 -0
  11. package/lib/installer.js +253 -0
  12. package/lib/platform.js +108 -0
  13. package/lib/symlink.js +142 -0
  14. package/lib/utils.js +270 -0
  15. package/package.json +67 -0
  16. package/skills/alloy-expert/SKILL.md +247 -0
  17. package/skills/alloy-expert/assets/ControllerAutoCleanup.js +182 -0
  18. package/skills/alloy-expert/references/alloy-structure.md +381 -0
  19. package/skills/alloy-expert/references/anti-patterns.md +133 -0
  20. package/skills/alloy-expert/references/code-conventions.md +469 -0
  21. package/skills/alloy-expert/references/contracts.md +280 -0
  22. package/skills/alloy-expert/references/controller-patterns.md +520 -0
  23. package/skills/alloy-expert/references/error-handling.md +484 -0
  24. package/skills/alloy-expert/references/examples.md +735 -0
  25. package/skills/alloy-expert/references/migration-patterns.md +298 -0
  26. package/skills/alloy-expert/references/patterns.md +448 -0
  27. package/skills/alloy-expert/references/performance-patterns.md +855 -0
  28. package/skills/alloy-expert/references/security-patterns.md +847 -0
  29. package/skills/alloy-expert/references/state-management.md +779 -0
  30. package/skills/alloy-expert/references/testing.md +872 -0
  31. package/skills/alloy-guides/SKILL.md +214 -0
  32. package/skills/alloy-guides/references/CLI_TASKS.md +243 -0
  33. package/skills/alloy-guides/references/CONCEPTS.md +191 -0
  34. package/skills/alloy-guides/references/CONTROLLERS.md +298 -0
  35. package/skills/alloy-guides/references/MODELS.md +1028 -0
  36. package/skills/alloy-guides/references/PURGETSS.md +56 -0
  37. package/skills/alloy-guides/references/VIEWS_DYNAMIC.md +242 -0
  38. package/skills/alloy-guides/references/VIEWS_STYLES.md +388 -0
  39. package/skills/alloy-guides/references/VIEWS_WITHOUT_CONTROLLERS.md +109 -0
  40. package/skills/alloy-guides/references/VIEWS_XML.md +558 -0
  41. package/skills/alloy-guides/references/WIDGETS.md +176 -0
  42. package/skills/alloy-howtos/SKILL.md +203 -0
  43. package/skills/alloy-howtos/references/best_practices.md +138 -0
  44. package/skills/alloy-howtos/references/cli_reference.md +253 -0
  45. package/skills/alloy-howtos/references/config_files.md +87 -0
  46. package/skills/alloy-howtos/references/custom_tags.md +147 -0
  47. package/skills/alloy-howtos/references/debugging_troubleshooting.md +101 -0
  48. package/skills/alloy-howtos/references/samples.md +167 -0
  49. package/skills/purgetss/SKILL.md +442 -0
  50. package/skills/purgetss/assets/purgetss.config.cjs +17 -0
  51. package/skills/purgetss/references/EXAMPLES.md +247 -0
  52. package/skills/purgetss/references/animation-system.md +1294 -0
  53. package/skills/purgetss/references/apply-directive.md +375 -0
  54. package/skills/purgetss/references/arbitrary-values.md +612 -0
  55. package/skills/purgetss/references/class-index.md +1350 -0
  56. package/skills/purgetss/references/cli-commands.md +948 -0
  57. package/skills/purgetss/references/configurable-properties.md +654 -0
  58. package/skills/purgetss/references/custom-rules.md +161 -0
  59. package/skills/purgetss/references/customization-deep-dive.md +722 -0
  60. package/skills/purgetss/references/dynamic-component-creation.md +489 -0
  61. package/skills/purgetss/references/grid-layout.md +455 -0
  62. package/skills/purgetss/references/icon-fonts.md +609 -0
  63. package/skills/purgetss/references/installation-setup.md +366 -0
  64. package/skills/purgetss/references/opacity-modifier.md +291 -0
  65. package/skills/purgetss/references/platform-modifiers.md +479 -0
  66. package/skills/purgetss/references/smart-mappings.md +42 -0
  67. package/skills/purgetss/references/titanium-resets.md +359 -0
  68. package/skills/purgetss/references/ui-ux-design.md +1526 -0
  69. package/skills/ti-guides/SKILL.md +94 -0
  70. package/skills/ti-guides/references/advanced-data-and-images.md +19 -0
  71. package/skills/ti-guides/references/alloy-cli-advanced.md +84 -0
  72. package/skills/ti-guides/references/alloy-data-mastery.md +29 -0
  73. package/skills/ti-guides/references/alloy-widgets-and-themes.md +19 -0
  74. package/skills/ti-guides/references/android-manifest.md +97 -0
  75. package/skills/ti-guides/references/app-distribution.md +258 -0
  76. package/skills/ti-guides/references/application-frameworks.md +377 -0
  77. package/skills/ti-guides/references/cli-reference.md +402 -0
  78. package/skills/ti-guides/references/coding-best-practices.md +102 -0
  79. package/skills/ti-guides/references/commonjs-advanced.md +134 -0
  80. package/skills/ti-guides/references/hello-world.md +100 -0
  81. package/skills/ti-guides/references/hyperloop-native-access.md +62 -0
  82. package/skills/ti-guides/references/javascript-primer.md +411 -0
  83. package/skills/ti-guides/references/reserved-words.md +36 -0
  84. package/skills/ti-guides/references/resources.md +183 -0
  85. package/skills/ti-guides/references/style-and-conventions.md +48 -0
  86. package/skills/ti-guides/references/tiapp-config.md +609 -0
  87. package/skills/ti-howtos/SKILL.md +174 -0
  88. package/skills/ti-howtos/references/android-platform-deep-dives.md +658 -0
  89. package/skills/ti-howtos/references/automation-fastlane-appium.md +95 -0
  90. package/skills/ti-howtos/references/buffer-codec-streams.md +140 -0
  91. package/skills/ti-howtos/references/cross-platform-development.md +348 -0
  92. package/skills/ti-howtos/references/debugging-profiling.md +543 -0
  93. package/skills/ti-howtos/references/extending-titanium.md +723 -0
  94. package/skills/ti-howtos/references/google-maps-v2.md +169 -0
  95. package/skills/ti-howtos/references/ios-map-kit.md +143 -0
  96. package/skills/ti-howtos/references/ios-platform-deep-dives.md +783 -0
  97. package/skills/ti-howtos/references/local-data-sources.md +301 -0
  98. package/skills/ti-howtos/references/location-and-maps.md +252 -0
  99. package/skills/ti-howtos/references/media-apis.md +210 -0
  100. package/skills/ti-howtos/references/notification-services.md +599 -0
  101. package/skills/ti-howtos/references/remote-data-sources.md +349 -0
  102. package/skills/ti-howtos/references/tutorials.md +502 -0
  103. package/skills/ti-howtos/references/using-modules.md +237 -0
  104. package/skills/ti-howtos/references/web-content-integration.md +307 -0
  105. package/skills/ti-howtos/references/webpack-build-pipeline.md +78 -0
  106. package/skills/ti-ui/SKILL.md +179 -0
  107. package/skills/ti-ui/references/accessibility-deep-dive.md +242 -0
  108. package/skills/ti-ui/references/animation-and-matrices.md +599 -0
  109. package/skills/ti-ui/references/application-structures.md +655 -0
  110. package/skills/ti-ui/references/custom-fonts-styling.md +579 -0
  111. package/skills/ti-ui/references/event-handling.md +393 -0
  112. package/skills/ti-ui/references/gestures.md +473 -0
  113. package/skills/ti-ui/references/icons-and-splash-screens.md +409 -0
  114. package/skills/ti-ui/references/layouts-and-positioning.md +462 -0
  115. package/skills/ti-ui/references/listviews-and-performance.md +619 -0
  116. package/skills/ti-ui/references/orientation.md +362 -0
  117. package/skills/ti-ui/references/platform-ui-android.md +635 -0
  118. package/skills/ti-ui/references/platform-ui-ios.md +469 -0
  119. package/skills/ti-ui/references/scrolling-views.md +252 -0
  120. 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 |