@capgo/capacitor-native-audio 8.4.3
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/CapgoCapacitorNativeAudio.podspec +16 -0
- package/LICENSE +373 -0
- package/Package.swift +31 -0
- package/README.md +1229 -0
- package/android/build.gradle +89 -0
- package/android/src/main/AndroidManifest.xml +3 -0
- package/android/src/main/java/ee/forgr/audio/AudioAsset.java +611 -0
- package/android/src/main/java/ee/forgr/audio/AudioCompletionListener.java +5 -0
- package/android/src/main/java/ee/forgr/audio/AudioDispatcher.java +208 -0
- package/android/src/main/java/ee/forgr/audio/Constant.java +36 -0
- package/android/src/main/java/ee/forgr/audio/HlsAvailabilityChecker.java +84 -0
- package/android/src/main/java/ee/forgr/audio/Logger.java +55 -0
- package/android/src/main/java/ee/forgr/audio/NativeAudio.java +2022 -0
- package/android/src/main/java/ee/forgr/audio/RemoteAudioAsset.java +886 -0
- package/android/src/main/java/ee/forgr/audio/StreamAudioAsset.java +708 -0
- package/android/src/main/res/values/colors.xml +3 -0
- package/android/src/main/res/values/strings.xml +3 -0
- package/android/src/main/res/values/styles.xml +3 -0
- package/dist/docs.json +1470 -0
- package/dist/esm/audio-asset.d.ts +4 -0
- package/dist/esm/audio-asset.js +6 -0
- package/dist/esm/audio-asset.js.map +1 -0
- package/dist/esm/definitions.d.ts +597 -0
- package/dist/esm/definitions.js +2 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +4 -0
- package/dist/esm/index.js +7 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +82 -0
- package/dist/esm/web.js +553 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +571 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +574 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Sources/NativeAudioPlugin/AudioAsset+Fade.swift +157 -0
- package/ios/Sources/NativeAudioPlugin/AudioAsset.swift +403 -0
- package/ios/Sources/NativeAudioPlugin/Constant.swift +52 -0
- package/ios/Sources/NativeAudioPlugin/Logger.swift +43 -0
- package/ios/Sources/NativeAudioPlugin/Plugin.swift +1786 -0
- package/ios/Sources/NativeAudioPlugin/RemoteAudioAsset+Fade.swift +152 -0
- package/ios/Sources/NativeAudioPlugin/RemoteAudioAsset.swift +405 -0
- package/ios/Tests/NativeAudioPluginTests/PluginTests.swift +648 -0
- package/ios/Tests/README.md +39 -0
- package/package.json +101 -0
- package/scripts/configure-dependencies.js +251 -0
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
import XCTest
|
|
2
|
+
import Capacitor
|
|
3
|
+
import AVFoundation
|
|
4
|
+
@testable import NativeAudioPlugin
|
|
5
|
+
|
|
6
|
+
// swiftlint:disable file_length
|
|
7
|
+
class PluginTests: XCTestCase {
|
|
8
|
+
|
|
9
|
+
var plugin = NativeAudio()
|
|
10
|
+
var tempFileURL = URL(fileURLWithPath: NSTemporaryDirectory().appending("testAudio.wav"))
|
|
11
|
+
var testAssetId = "testAssetId"
|
|
12
|
+
var testRemoteAssetId = "testRemoteAssetId"
|
|
13
|
+
|
|
14
|
+
override func setUp() {
|
|
15
|
+
super.setUp()
|
|
16
|
+
plugin = NativeAudio()
|
|
17
|
+
plugin.isRunningTests = true
|
|
18
|
+
|
|
19
|
+
// Create a temporary audio file for testing
|
|
20
|
+
let audioFilePath = NSTemporaryDirectory().appending("testAudio.wav")
|
|
21
|
+
tempFileURL = URL(fileURLWithPath: audioFilePath)
|
|
22
|
+
|
|
23
|
+
// Create a simple test audio file if needed
|
|
24
|
+
if !FileManager.default.fileExists(atPath: audioFilePath) {
|
|
25
|
+
createTestAudioFile(at: audioFilePath)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
override func tearDown() {
|
|
30
|
+
// Clean up any audio assets
|
|
31
|
+
plugin.executeOnAudioQueue {
|
|
32
|
+
if let asset = self.plugin.audioList[self.testAssetId] as? AudioAsset {
|
|
33
|
+
asset.unload()
|
|
34
|
+
}
|
|
35
|
+
if let asset = self.plugin.audioList[self.testRemoteAssetId] as? RemoteAudioAsset {
|
|
36
|
+
asset.unload()
|
|
37
|
+
}
|
|
38
|
+
self.plugin.audioList.removeAll()
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Try to delete the temporary file
|
|
42
|
+
try? FileManager.default.removeItem(at: tempFileURL)
|
|
43
|
+
|
|
44
|
+
plugin = nil
|
|
45
|
+
super.tearDown()
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Helper method to create a simple test audio file
|
|
49
|
+
private func createTestAudioFile(at path: String) {
|
|
50
|
+
// This is a placeholder for a real implementation
|
|
51
|
+
// In a real scenario, you would create a small audio file for testing
|
|
52
|
+
// For now, we'll just create an empty file
|
|
53
|
+
FileManager.default.createFile(atPath: path, contents: Data(), attributes: nil)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
private func makeCall(callbackId: String, options: [String: Any], onErrorMessage: String) -> CAPPluginCall {
|
|
57
|
+
guard let call = CAPPluginCall(callbackId: callbackId, options: options, success: { _, _ in }, error: { _ in }) else {
|
|
58
|
+
XCTFail("Failed to create CAPPluginCall: \(onErrorMessage)")
|
|
59
|
+
fatalError("Failed to create CAPPluginCall")
|
|
60
|
+
}
|
|
61
|
+
return call
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
extension PluginTests {
|
|
67
|
+
|
|
68
|
+
func testAudioAssetInitialization() {
|
|
69
|
+
let expectation = XCTestExpectation(description: "Initialize AudioAsset")
|
|
70
|
+
|
|
71
|
+
plugin.executeOnAudioQueue {
|
|
72
|
+
// Create an audio asset
|
|
73
|
+
let asset = AudioAsset(
|
|
74
|
+
owner: self.plugin,
|
|
75
|
+
withAssetId: self.testAssetId,
|
|
76
|
+
withPath: self.tempFileURL.path,
|
|
77
|
+
withChannels: 1,
|
|
78
|
+
withVolume: 0.5
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
// Add it to the plugin's audio list
|
|
82
|
+
self.plugin.audioList[self.testAssetId] = asset
|
|
83
|
+
|
|
84
|
+
// Verify initial values
|
|
85
|
+
XCTAssertEqual(asset.assetId, self.testAssetId)
|
|
86
|
+
XCTAssertEqual(asset.initialVolume, 0.5)
|
|
87
|
+
|
|
88
|
+
expectation.fulfill()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
wait(for: [expectation], timeout: 5.0)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
func testAudioAssetVolumeControl() {
|
|
95
|
+
let expectation = XCTestExpectation(description: "Test volume control")
|
|
96
|
+
|
|
97
|
+
plugin.executeOnAudioQueue {
|
|
98
|
+
// Create an audio asset
|
|
99
|
+
let asset = AudioAsset(
|
|
100
|
+
owner: self.plugin,
|
|
101
|
+
withAssetId: self.testAssetId,
|
|
102
|
+
withPath: self.tempFileURL.path,
|
|
103
|
+
withChannels: 1,
|
|
104
|
+
withVolume: 1.0
|
|
105
|
+
)
|
|
106
|
+
|
|
107
|
+
// Add it to the plugin's audio list
|
|
108
|
+
self.plugin.audioList[self.testAssetId] = asset
|
|
109
|
+
|
|
110
|
+
// Test setting volume
|
|
111
|
+
let testVolume: Float = 0.7
|
|
112
|
+
asset.setVolume(volume: NSNumber(value: testVolume), fadeDuration: 0)
|
|
113
|
+
|
|
114
|
+
// We can't directly check player.volume as it may take time to set
|
|
115
|
+
// So we'll just verify the method doesn't crash
|
|
116
|
+
|
|
117
|
+
expectation.fulfill()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
wait(for: [expectation], timeout: 5.0)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
func testRemoteAudioAssetInitialization() {
|
|
124
|
+
let expectation = XCTestExpectation(description: "Initialize RemoteAudioAsset")
|
|
125
|
+
|
|
126
|
+
// Use a publicly accessible test audio URL
|
|
127
|
+
let testURL = "https://file-examples.com/storage/fe5947fd2362a2f06a86851/2017/11/file_example_MP3_700KB.mp3"
|
|
128
|
+
|
|
129
|
+
plugin.executeOnAudioQueue {
|
|
130
|
+
// Create a remote audio asset
|
|
131
|
+
let asset = RemoteAudioAsset(
|
|
132
|
+
owner: self.plugin,
|
|
133
|
+
withAssetId: self.testRemoteAssetId,
|
|
134
|
+
withPath: testURL,
|
|
135
|
+
withChannels: 1,
|
|
136
|
+
withVolume: 0.6,
|
|
137
|
+
withHeaders: nil
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
// Add it to the plugin's audio list
|
|
141
|
+
self.plugin.audioList[self.testRemoteAssetId] = asset
|
|
142
|
+
|
|
143
|
+
// Verify initial values
|
|
144
|
+
XCTAssertEqual(asset.assetId, self.testRemoteAssetId)
|
|
145
|
+
XCTAssertEqual(asset.initialVolume, 0.6)
|
|
146
|
+
XCTAssertNotNil(asset.asset, "AVURLAsset should be created")
|
|
147
|
+
|
|
148
|
+
expectation.fulfill()
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
wait(for: [expectation], timeout: 5.0)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
func testPluginPreloadMethod() {
|
|
155
|
+
// Create a plugin call to test the preload method
|
|
156
|
+
let call = makeCall(callbackId: "test", options: [
|
|
157
|
+
"assetId": testAssetId,
|
|
158
|
+
"assetPath": tempFileURL.path,
|
|
159
|
+
"volume": 0.8,
|
|
160
|
+
"audioChannelNum": 2
|
|
161
|
+
], onErrorMessage: "Preload call creation")
|
|
162
|
+
|
|
163
|
+
// Call the plugin method
|
|
164
|
+
plugin.preload(call)
|
|
165
|
+
|
|
166
|
+
// Verify the asset was loaded by checking if it exists in the audioList
|
|
167
|
+
plugin.executeOnAudioQueue {
|
|
168
|
+
XCTAssertNotNil(self.plugin.audioList[self.testAssetId])
|
|
169
|
+
if let asset = self.plugin.audioList[self.testAssetId] as? AudioAsset {
|
|
170
|
+
XCTAssertEqual(asset.assetId, self.testAssetId)
|
|
171
|
+
XCTAssertEqual(asset.initialVolume, 0.8)
|
|
172
|
+
} else {
|
|
173
|
+
XCTFail("Asset should be of type AudioAsset")
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
func testFadeEffects() {
|
|
179
|
+
let expectation = XCTestExpectation(description: "Test fade effects")
|
|
180
|
+
|
|
181
|
+
plugin.executeOnAudioQueue {
|
|
182
|
+
// Create an audio asset
|
|
183
|
+
let asset = AudioAsset(
|
|
184
|
+
owner: self.plugin,
|
|
185
|
+
withAssetId: self.testAssetId,
|
|
186
|
+
withPath: self.tempFileURL.path,
|
|
187
|
+
withChannels: 1,
|
|
188
|
+
withVolume: 1.0
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
// Test fade functionality (just make sure it doesn't crash)
|
|
192
|
+
asset.playWithFade(time: 0)
|
|
193
|
+
|
|
194
|
+
// Wait a short time for fade to start
|
|
195
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
|
196
|
+
// Then test stop with fade
|
|
197
|
+
asset.stopWithFade()
|
|
198
|
+
|
|
199
|
+
// Wait for fade to complete
|
|
200
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
|
201
|
+
expectation.fulfill()
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
wait(for: [expectation], timeout: 5.0)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Test the ClearCache functionality
|
|
210
|
+
func testClearCache() {
|
|
211
|
+
// This is mostly a method call test to ensure it doesn't crash
|
|
212
|
+
RemoteAudioAsset.clearCache()
|
|
213
|
+
|
|
214
|
+
// We can't easily verify the cache was cleared without complex setup,
|
|
215
|
+
// but we can ensure the method completes without errors
|
|
216
|
+
XCTAssertTrue(true)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Test notification observer pattern in RemoteAudioAsset
|
|
220
|
+
func testNotificationObserverPattern() {
|
|
221
|
+
let expectation = XCTestExpectation(description: "Test notification observer")
|
|
222
|
+
|
|
223
|
+
// Use a publicly accessible test audio URL
|
|
224
|
+
let testURL = "https://file-examples.com/storage/fe5947fd2362a2f06a86851/2017/11/file_example_MP3_700KB.mp3"
|
|
225
|
+
|
|
226
|
+
plugin.executeOnAudioQueue {
|
|
227
|
+
// Create a remote audio asset
|
|
228
|
+
let asset = RemoteAudioAsset(
|
|
229
|
+
owner: self.plugin,
|
|
230
|
+
withAssetId: self.testRemoteAssetId,
|
|
231
|
+
withPath: testURL,
|
|
232
|
+
withChannels: 1,
|
|
233
|
+
withVolume: 0.6,
|
|
234
|
+
withHeaders: nil
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
// Add it to the plugin's audio list
|
|
238
|
+
self.plugin.audioList[self.testRemoteAssetId] = asset
|
|
239
|
+
|
|
240
|
+
// Verify initial values
|
|
241
|
+
XCTAssertEqual(asset.notificationObservers.count, 0, "Should start with zero notification observers")
|
|
242
|
+
|
|
243
|
+
// Test the resume method which sets up a notification observer
|
|
244
|
+
asset.resume()
|
|
245
|
+
|
|
246
|
+
// Check that a notification observer was added
|
|
247
|
+
XCTAssertGreaterThan(asset.notificationObservers.count, 0, "Should have added notification observers")
|
|
248
|
+
|
|
249
|
+
// Now test cleanup
|
|
250
|
+
asset.cleanupNotificationObservers()
|
|
251
|
+
XCTAssertEqual(asset.notificationObservers.count, 0, "Should have removed all notification observers")
|
|
252
|
+
|
|
253
|
+
expectation.fulfill()
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
wait(for: [expectation], timeout: 5.0)
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// Test the fade timer functionality, which was a key part of our fixes
|
|
260
|
+
func testFadeTimerFunctionality() {
|
|
261
|
+
let expectation = XCTestExpectation(description: "Test fade timer")
|
|
262
|
+
|
|
263
|
+
plugin.executeOnAudioQueue {
|
|
264
|
+
// Create an audio asset
|
|
265
|
+
let asset = AudioAsset(
|
|
266
|
+
owner: self.plugin,
|
|
267
|
+
withAssetId: self.testAssetId,
|
|
268
|
+
withPath: self.tempFileURL.path,
|
|
269
|
+
withChannels: 1,
|
|
270
|
+
withVolume: 1.0
|
|
271
|
+
)
|
|
272
|
+
|
|
273
|
+
// Ensure the fade timer is nil initially
|
|
274
|
+
XCTAssertNil(asset.fadeTimer, "Fade timer should be nil initially")
|
|
275
|
+
|
|
276
|
+
// Access the private method using reflection
|
|
277
|
+
// (this is a test-only approach to access private methods)
|
|
278
|
+
let selector = NSSelectorFromString("startVolumeRamp:to:player:")
|
|
279
|
+
if asset.responds(to: selector) {
|
|
280
|
+
// Create a test mock for AVAudioPlayer
|
|
281
|
+
guard let player = asset.channels.first else {
|
|
282
|
+
XCTFail("No audio player available")
|
|
283
|
+
expectation.fulfill()
|
|
284
|
+
return
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// Set initial volume
|
|
288
|
+
player.volume = 1.0
|
|
289
|
+
|
|
290
|
+
// Invoke using performSelector - Note: perform only supports up to 2 'with:' parameters
|
|
291
|
+
// This test is disabled as the selector requires 3 parameters
|
|
292
|
+
// asset.perform(selector, with: NSNumber(value: 1.0), with: NSNumber(value: 0.0))
|
|
293
|
+
|
|
294
|
+
// Check that the fade timer was created
|
|
295
|
+
XCTAssertNotNil(asset.fadeTimer, "Fade timer should be created")
|
|
296
|
+
|
|
297
|
+
// Wait for fade to complete
|
|
298
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 1.0) {
|
|
299
|
+
// Fade should be complete, timer should be nil again
|
|
300
|
+
XCTAssertNil(asset.fadeTimer, "Fade timer should be nil after completion")
|
|
301
|
+
XCTAssertEqual(player.volume, 0.0, "Volume should be 0 after fade out")
|
|
302
|
+
|
|
303
|
+
expectation.fulfill()
|
|
304
|
+
}
|
|
305
|
+
} else {
|
|
306
|
+
XCTFail("startVolumeRamp method not available")
|
|
307
|
+
expectation.fulfill()
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
wait(for: [expectation], timeout: 5.0)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// MARK: - PlayOnce Tests
|
|
315
|
+
|
|
316
|
+
func testPlayOnceWithAutoPlay() {
|
|
317
|
+
let expectation = XCTestExpectation(description: "PlayOnce with auto-play")
|
|
318
|
+
var returnedAssetId: String?
|
|
319
|
+
|
|
320
|
+
let call = makeCall(callbackId: "test", options: [
|
|
321
|
+
"assetPath": tempFileURL.path,
|
|
322
|
+
"volume": 1.0,
|
|
323
|
+
"isUrl": true,
|
|
324
|
+
"autoPlay": true
|
|
325
|
+
], onErrorMessage: "playOnce auto-play call")
|
|
326
|
+
_ = returnedAssetId
|
|
327
|
+
|
|
328
|
+
plugin.playOnce(call)
|
|
329
|
+
|
|
330
|
+
plugin.executeOnAudioQueue {
|
|
331
|
+
|
|
332
|
+
// Wait for async operations
|
|
333
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
334
|
+
// Verify asset was created and is in playOnceAssets
|
|
335
|
+
self.plugin.executeOnAudioQueue {
|
|
336
|
+
let playOnceAssets = self.plugin.playOnceAssets
|
|
337
|
+
XCTAssertTrue(playOnceAssets.count > 0, "Should have created a playOnce asset")
|
|
338
|
+
|
|
339
|
+
// Verify the returned assetId matches an entry in playOnceAssets and audioList
|
|
340
|
+
if let assetId = returnedAssetId {
|
|
341
|
+
XCTAssertTrue(playOnceAssets.contains(assetId), "Returned assetId should be in playOnceAssets")
|
|
342
|
+
XCTAssertNotNil(self.plugin.audioList[assetId], "Returned assetId should have corresponding AudioAsset")
|
|
343
|
+
} else {
|
|
344
|
+
XCTFail("Should have returned an assetId")
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
expectation.fulfill()
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
wait(for: [expectation], timeout: 3.0)
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
func testPlayOnceWithoutAutoPlay() {
|
|
356
|
+
let expectation = XCTestExpectation(description: "PlayOnce without auto-play")
|
|
357
|
+
|
|
358
|
+
let call = makeCall(callbackId: "test", options: [
|
|
359
|
+
"assetPath": tempFileURL.path,
|
|
360
|
+
"volume": 0.8,
|
|
361
|
+
"isUrl": true,
|
|
362
|
+
"autoPlay": false
|
|
363
|
+
], onErrorMessage: "playOnce no-autoplay call")
|
|
364
|
+
|
|
365
|
+
plugin.playOnce(call)
|
|
366
|
+
|
|
367
|
+
plugin.executeOnAudioQueue {
|
|
368
|
+
|
|
369
|
+
// Wait for async operations
|
|
370
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
371
|
+
self.plugin.executeOnAudioQueue {
|
|
372
|
+
// Asset should be created but not playing
|
|
373
|
+
let playOnceAssets = self.plugin.playOnceAssets
|
|
374
|
+
XCTAssertTrue(playOnceAssets.count > 0, "Should have created a playOnce asset")
|
|
375
|
+
|
|
376
|
+
if let assetId = playOnceAssets.first,
|
|
377
|
+
let asset = self.plugin.audioList[assetId] as? AudioAsset {
|
|
378
|
+
// Verify asset exists but is not automatically playing
|
|
379
|
+
XCTAssertFalse(asset.channels.isEmpty, "Asset should have channels")
|
|
380
|
+
|
|
381
|
+
// Verify player is not playing when autoPlay is false
|
|
382
|
+
if let player = asset.channels.first {
|
|
383
|
+
XCTAssertFalse(player.isPlaying, "Player should not be playing when autoPlay is false")
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
expectation.fulfill()
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
wait(for: [expectation], timeout: 3.0)
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
func testPlayOnceCleanupAfterCompletion() {
|
|
396
|
+
let expectation = XCTestExpectation(description: "PlayOnce cleanup after completion")
|
|
397
|
+
|
|
398
|
+
let call = makeCall(callbackId: "test", options: [
|
|
399
|
+
"assetPath": tempFileURL.path,
|
|
400
|
+
"volume": 1.0,
|
|
401
|
+
"isUrl": true,
|
|
402
|
+
"autoPlay": true
|
|
403
|
+
], onErrorMessage: "playOnce cleanup call")
|
|
404
|
+
|
|
405
|
+
plugin.playOnce(call)
|
|
406
|
+
|
|
407
|
+
plugin.executeOnAudioQueue {
|
|
408
|
+
|
|
409
|
+
// Get the asset ID
|
|
410
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
|
411
|
+
self.plugin.executeOnAudioQueue {
|
|
412
|
+
guard let assetId = self.plugin.playOnceAssets.first else {
|
|
413
|
+
XCTFail("No playOnce asset was created")
|
|
414
|
+
expectation.fulfill()
|
|
415
|
+
return
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Verify asset exists
|
|
419
|
+
XCTAssertNotNil(self.plugin.audioList[assetId], "Asset should exist")
|
|
420
|
+
|
|
421
|
+
// Simulate completion
|
|
422
|
+
if let asset = self.plugin.audioList[assetId] as? AudioAsset {
|
|
423
|
+
asset.onComplete?()
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Wait for cleanup
|
|
427
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
428
|
+
self.plugin.executeOnAudioQueue {
|
|
429
|
+
// Verify cleanup occurred
|
|
430
|
+
XCTAssertNil(self.plugin.audioList[assetId], "Asset should be removed after cleanup")
|
|
431
|
+
XCTAssertFalse(self.plugin.playOnceAssets.contains(assetId), "AssetId should be removed from playOnceAssets")
|
|
432
|
+
|
|
433
|
+
expectation.fulfill()
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
wait(for: [expectation], timeout: 5.0)
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
func testPlayOnceWithDeleteAfterPlay() {
|
|
444
|
+
let expectation = XCTestExpectation(description: "PlayOnce with file deletion")
|
|
445
|
+
|
|
446
|
+
// Create a temporary file that can be deleted
|
|
447
|
+
let deletableFilePath = NSTemporaryDirectory().appending("deletableAudio.wav")
|
|
448
|
+
let deletableURL = URL(fileURLWithPath: deletableFilePath)
|
|
449
|
+
|
|
450
|
+
// Copy test file to deletable location
|
|
451
|
+
do {
|
|
452
|
+
// Remove existing file if present
|
|
453
|
+
if FileManager.default.fileExists(atPath: deletableFilePath) {
|
|
454
|
+
try FileManager.default.removeItem(at: deletableURL)
|
|
455
|
+
}
|
|
456
|
+
try FileManager.default.copyItem(at: tempFileURL, to: deletableURL)
|
|
457
|
+
} catch {
|
|
458
|
+
XCTFail("Failed to set up deletable file: \(error)")
|
|
459
|
+
expectation.fulfill()
|
|
460
|
+
return
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
let call = makeCall(callbackId: "test", options: [
|
|
464
|
+
"assetPath": deletableURL.absoluteString,
|
|
465
|
+
"volume": 1.0,
|
|
466
|
+
"isUrl": true,
|
|
467
|
+
"autoPlay": true,
|
|
468
|
+
"deleteAfterPlay": true
|
|
469
|
+
], onErrorMessage: "playOnce delete-after-play call")
|
|
470
|
+
|
|
471
|
+
plugin.playOnce(call)
|
|
472
|
+
|
|
473
|
+
plugin.executeOnAudioQueue {
|
|
474
|
+
|
|
475
|
+
// Wait for asset creation
|
|
476
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
|
477
|
+
self.plugin.executeOnAudioQueue {
|
|
478
|
+
guard let assetId = self.plugin.playOnceAssets.first else {
|
|
479
|
+
XCTFail("No playOnce asset was created")
|
|
480
|
+
expectation.fulfill()
|
|
481
|
+
return
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
// Verify file exists before completion
|
|
485
|
+
XCTAssertTrue(FileManager.default.fileExists(atPath: deletableFilePath), "File should exist before cleanup")
|
|
486
|
+
|
|
487
|
+
// Simulate completion
|
|
488
|
+
if let asset = self.plugin.audioList[assetId] as? AudioAsset {
|
|
489
|
+
asset.onComplete?()
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
// Wait for cleanup and deletion
|
|
493
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
494
|
+
// File should be deleted after cleanup
|
|
495
|
+
let fileExists = FileManager.default.fileExists(atPath: deletableFilePath)
|
|
496
|
+
XCTAssertFalse(fileExists, "File should be deleted after playOnce completion")
|
|
497
|
+
|
|
498
|
+
expectation.fulfill()
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
wait(for: [expectation], timeout: 5.0)
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
extension PluginTests {
|
|
510
|
+
|
|
511
|
+
func testPlayOnceWithNotificationMetadata() {
|
|
512
|
+
let expectation = XCTestExpectation(description: "PlayOnce with notification metadata")
|
|
513
|
+
|
|
514
|
+
let call = makeCall(callbackId: "test", options: [
|
|
515
|
+
"assetPath": tempFileURL.path,
|
|
516
|
+
"volume": 1.0,
|
|
517
|
+
"isUrl": true,
|
|
518
|
+
"autoPlay": false,
|
|
519
|
+
"notificationMetadata": [
|
|
520
|
+
"title": "Test Song",
|
|
521
|
+
"artist": "Test Artist",
|
|
522
|
+
"album": "Test Album",
|
|
523
|
+
"artworkUrl": "https://example.com/artwork.jpg"
|
|
524
|
+
]
|
|
525
|
+
], onErrorMessage: "playOnce metadata call")
|
|
526
|
+
|
|
527
|
+
plugin.playOnce(call)
|
|
528
|
+
|
|
529
|
+
plugin.executeOnAudioQueue {
|
|
530
|
+
|
|
531
|
+
// Wait for async operations
|
|
532
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
533
|
+
self.plugin.executeOnAudioQueue {
|
|
534
|
+
guard let assetId = self.plugin.playOnceAssets.first else {
|
|
535
|
+
XCTFail("No playOnce asset was created")
|
|
536
|
+
expectation.fulfill()
|
|
537
|
+
return
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
// Verify notification metadata was stored
|
|
541
|
+
if let metadata = self.plugin.notificationMetadataMap[assetId] {
|
|
542
|
+
XCTAssertEqual(metadata["title"], "Test Song", "Title should be stored")
|
|
543
|
+
XCTAssertEqual(metadata["artist"], "Test Artist", "Artist should be stored")
|
|
544
|
+
XCTAssertEqual(metadata["album"], "Test Album", "Album should be stored")
|
|
545
|
+
XCTAssertEqual(metadata["artworkUrl"], "https://example.com/artwork.jpg", "Artwork URL should be stored")
|
|
546
|
+
} else {
|
|
547
|
+
XCTFail("Notification metadata should be stored")
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
expectation.fulfill()
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
wait(for: [expectation], timeout: 3.0)
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
func testPlayOnceErrorHandlingAndCleanup() {
|
|
559
|
+
let expectation = XCTestExpectation(description: "PlayOnce error handling and cleanup")
|
|
560
|
+
|
|
561
|
+
// Use an invalid file path to trigger error
|
|
562
|
+
let call = makeCall(callbackId: "test", options: [
|
|
563
|
+
"assetPath": "/invalid/path/to/nonexistent.wav",
|
|
564
|
+
"volume": 1.0,
|
|
565
|
+
"isUrl": true,
|
|
566
|
+
"autoPlay": true
|
|
567
|
+
], onErrorMessage: "playOnce error handling call")
|
|
568
|
+
|
|
569
|
+
plugin.playOnce(call)
|
|
570
|
+
|
|
571
|
+
plugin.executeOnAudioQueue {
|
|
572
|
+
// Capture any assetId that might have been created
|
|
573
|
+
let initialAssetIds = self.plugin.playOnceAssets
|
|
574
|
+
|
|
575
|
+
// Wait for error handling
|
|
576
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
577
|
+
self.plugin.executeOnAudioQueue {
|
|
578
|
+
// Verify cleanup occurred even on failure
|
|
579
|
+
// Any asset created should be cleaned up from audioList
|
|
580
|
+
for assetId in initialAssetIds {
|
|
581
|
+
XCTAssertNil(self.plugin.audioList[assetId], "Failed asset should be cleaned up from audioList")
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
// And no dangling playOnce IDs should remain
|
|
585
|
+
XCTAssertTrue(self.plugin.playOnceAssets.isEmpty, "playOnce assets should be cleaned up on error")
|
|
586
|
+
|
|
587
|
+
expectation.fulfill()
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
wait(for: [expectation], timeout: 3.0)
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
func testPlayOnceReturnsUniqueAssetId() {
|
|
596
|
+
let expectation = XCTestExpectation(description: "PlayOnce returns unique asset ID")
|
|
597
|
+
|
|
598
|
+
var firstAssetId: String?
|
|
599
|
+
var secondAssetId: String?
|
|
600
|
+
|
|
601
|
+
let call1 = makeCall(callbackId: "test1", options: [
|
|
602
|
+
"assetPath": tempFileURL.path,
|
|
603
|
+
"volume": 1.0,
|
|
604
|
+
"isUrl": true,
|
|
605
|
+
"autoPlay": false
|
|
606
|
+
], onErrorMessage: "playOnce unique call1")
|
|
607
|
+
_ = firstAssetId
|
|
608
|
+
|
|
609
|
+
plugin.playOnce(call1)
|
|
610
|
+
|
|
611
|
+
// Create second playOnce
|
|
612
|
+
let call2 = makeCall(callbackId: "test2", options: [
|
|
613
|
+
"assetPath": tempFileURL.path,
|
|
614
|
+
"volume": 1.0,
|
|
615
|
+
"isUrl": true,
|
|
616
|
+
"autoPlay": false
|
|
617
|
+
], onErrorMessage: "playOnce unique call2")
|
|
618
|
+
_ = secondAssetId
|
|
619
|
+
|
|
620
|
+
plugin.playOnce(call2)
|
|
621
|
+
|
|
622
|
+
// Wait for both to complete
|
|
623
|
+
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
|
624
|
+
self.plugin.executeOnAudioQueue {
|
|
625
|
+
// Verify we got two distinct asset IDs from the public API
|
|
626
|
+
XCTAssertNotNil(firstAssetId, "First asset ID should exist")
|
|
627
|
+
XCTAssertNotNil(secondAssetId, "Second asset ID should exist")
|
|
628
|
+
XCTAssertNotEqual(firstAssetId, secondAssetId, "Asset IDs should be unique")
|
|
629
|
+
|
|
630
|
+
// Verify both have "playOnce_" prefix
|
|
631
|
+
XCTAssertTrue(firstAssetId?.hasPrefix("playOnce_") ?? false, "First asset ID should have playOnce prefix")
|
|
632
|
+
XCTAssertTrue(secondAssetId?.hasPrefix("playOnce_") ?? false, "Second asset ID should have playOnce prefix")
|
|
633
|
+
|
|
634
|
+
// Cross-check that both are present in playOnceAssets as internal consistency check
|
|
635
|
+
if let id1 = firstAssetId, let id2 = secondAssetId {
|
|
636
|
+
XCTAssertTrue(self.plugin.playOnceAssets.contains(id1), "First assetId should be tracked internally")
|
|
637
|
+
XCTAssertTrue(self.plugin.playOnceAssets.contains(id2), "Second assetId should be tracked internally")
|
|
638
|
+
XCTAssertEqual(self.plugin.playOnceAssets.count, 2, "Should have two playOnce assets tracked")
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
expectation.fulfill()
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
wait(for: [expectation], timeout: 5.0)
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
// swiftlint:enable file_length
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Native Audio Plugin Tests
|
|
2
|
+
|
|
3
|
+
This directory contains tests for the Capacitor Native Audio Plugin.
|
|
4
|
+
|
|
5
|
+
## Running Tests
|
|
6
|
+
|
|
7
|
+
The easiest way to run these tests is through Xcode:
|
|
8
|
+
|
|
9
|
+
1. Open the `Plugin.xcworkspace` file in Xcode
|
|
10
|
+
2. Select the "Plugin" scheme
|
|
11
|
+
3. Go to Product > Test (or press ⌘+U)
|
|
12
|
+
|
|
13
|
+
## Test Coverage
|
|
14
|
+
|
|
15
|
+
The tests cover the following functionality:
|
|
16
|
+
|
|
17
|
+
- Basic initialization of `AudioAsset` and `RemoteAudioAsset` classes
|
|
18
|
+
- Volume control
|
|
19
|
+
- Fade effects (fade in/out)
|
|
20
|
+
- Notification observer pattern
|
|
21
|
+
- Cache clearing
|
|
22
|
+
- Plugin preloading
|
|
23
|
+
|
|
24
|
+
## Notes for Test Development
|
|
25
|
+
|
|
26
|
+
- The tests use a temporary audio file created at runtime
|
|
27
|
+
- For remote audio tests, a publicly accessible MP3 file is used
|
|
28
|
+
- Some tests use reflection to access private methods (for testing purposes only)
|
|
29
|
+
- All tests run on the plugin's audio queue to maintain thread safety
|
|
30
|
+
|
|
31
|
+
## Troubleshooting
|
|
32
|
+
|
|
33
|
+
If tests fail:
|
|
34
|
+
|
|
35
|
+
1. Make sure the iOS simulator is available
|
|
36
|
+
2. Check that the audio session is properly configured
|
|
37
|
+
3. For remote tests, ensure network connectivity is available
|
|
38
|
+
|
|
39
|
+
To view detailed test logs, expand the test navigator in Xcode and click on the failing test.
|