Skip to content

Commit 63bba1a

Browse files
committed
feat: InternalCdpApi annotation, cdp tutorial, v0.2.3
1 parent 24b0ff0 commit 63bba1a

File tree

12 files changed

+264
-70
lines changed

12 files changed

+264
-70
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ To use kdriver, add the following to your `build.gradle.kts`:
4747

4848
```kotlin
4949
dependencies {
50-
implementation("dev.kdriver:core:0.2.2")
50+
implementation("dev.kdriver:core:0.2.3")
5151
}
5252
```
5353

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ plugins {
66

77
allprojects {
88
group = "dev.kdriver"
9-
version = "0.2.2"
9+
version = "0.2.3"
1010

1111
repositories {
1212
mavenCentral()

cdp/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ kotlin {
8080
sourceSets {
8181
all {
8282
languageSettings.apply {
83+
optIn("dev.kdriver.cdp.InternalCdpApi")
8384
optIn("kotlin.js.ExperimentalJsExport")
8485
}
8586
}

cdp/src/commonMain/kotlin/dev/kdriver/cdp/CDP.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,16 @@ import kotlin.reflect.KClass
99
*/
1010
interface CDP {
1111

12+
@InternalCdpApi
1213
val events: Flow<Message.Event>
14+
15+
@InternalCdpApi
1316
val responses: Flow<Message.Response>
1417

18+
@InternalCdpApi
1519
val generatedDomains: MutableMap<KClass<out Domain>, Domain>
1620

21+
@InternalCdpApi
1722
suspend fun callCommand(method: String, parameter: JsonElement?): JsonElement?
1823

1924
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package dev.kdriver.cdp
2+
3+
@RequiresOptIn(
4+
message = "This API is internal to the CDP library. We recommend using the public APIs instead.",
5+
level = RequiresOptIn.Level.ERROR
6+
)
7+
@Retention(AnnotationRetention.BINARY)
8+
annotation class InternalCdpApi

core/build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ kotlin {
5656
sourceSets {
5757
all {
5858
languageSettings.apply {
59+
optIn("dev.kdriver.cdp.InternalCdpApi")
5960
optIn("kotlin.js.ExperimentalJsExport")
6061
}
6162
}

core/src/commonMain/kotlin/dev/kdriver/core/connection/Connection.kt

Lines changed: 119 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,7 @@
11
package dev.kdriver.core.connection
22

33
import dev.kaccelero.serializers.Serialization
4-
import dev.kdriver.cdp.CDP
5-
import dev.kdriver.cdp.Domain
6-
import dev.kdriver.cdp.Message
7-
import dev.kdriver.cdp.Request
4+
import dev.kdriver.cdp.*
85
import dev.kdriver.cdp.domain.Target
96
import dev.kdriver.cdp.domain.target
107
import dev.kdriver.core.browser.Browser
@@ -47,36 +44,18 @@ open class Connection(
4744

4845
private var socketSubscription: Job? = null
4946

50-
private fun startListening() {
51-
socketSubscription?.cancel()
52-
socketSubscription = messageListeningScope.launch {
53-
try {
54-
for (frame in wsSession?.incoming ?: return@launch) {
55-
try {
56-
frame as? Frame.Text ?: continue
57-
val text = frame.readText()
58-
logger.debug("WS < CDP: ${text.take(debugStringLimit)}")
59-
val received = Serialization.json.decodeFromString<Message>(text)
60-
allMessages.emit(received)
61-
} catch (e: Exception) {
62-
logger.debug("WebSocket exception while receiving message: {}", e)
63-
}
64-
}
65-
} catch (e: Exception) {
66-
e.printStackTrace()
67-
// Handle disconnect, maybe trigger reconnect logic here
68-
}
69-
}
70-
}
71-
7247
private val currentIdMutex = Mutex()
7348
private var currentId = 0L
7449

7550
private var allMessages = MutableSharedFlow<Message>(extraBufferCapacity = eventsBufferSize)
7651

52+
@InternalCdpApi
7753
override val events: Flow<Message.Event> = allMessages.filterIsInstance()
54+
55+
@InternalCdpApi
7856
override val responses: Flow<Message.Response> = allMessages.filterIsInstance()
7957

58+
@InternalCdpApi
8059
override val generatedDomains: MutableMap<KClass<out Domain>, Domain> = mutableMapOf()
8160

8261
private suspend fun connect() {
@@ -93,6 +72,34 @@ open class Connection(
9372
startListening()
9473
}
9574

75+
private fun startListening() {
76+
socketSubscription?.cancel()
77+
socketSubscription = messageListeningScope.launch {
78+
try {
79+
for (frame in wsSession?.incoming ?: return@launch) {
80+
try {
81+
frame as? Frame.Text ?: continue
82+
val text = frame.readText()
83+
logger.debug("WS < CDP: ${text.take(debugStringLimit)}")
84+
val received = Serialization.json.decodeFromString<Message>(text)
85+
allMessages.emit(received)
86+
} catch (e: Exception) {
87+
logger.debug("WebSocket exception while receiving message: {}", e)
88+
}
89+
}
90+
} catch (e: Exception) {
91+
e.printStackTrace()
92+
// Handle disconnect, maybe trigger reconnect logic here
93+
}
94+
}
95+
}
96+
97+
/**
98+
* Internal method to call a CDP command.
99+
*
100+
* This should not be called directly, but rather through typed methods (like `cdp.network.enable()`).
101+
*/
102+
@InternalCdpApi
96103
override suspend fun callCommand(method: String, parameter: JsonElement?): JsonElement? {
97104
connect()
98105
val requestId = currentIdMutex.withLock { currentId++ }
@@ -104,13 +111,22 @@ open class Connection(
104111
return result.result
105112
}
106113

114+
/**
115+
* Closes the websocket connection. Should not be called manually by users.
116+
*/
117+
@InternalCdpApi
107118
suspend fun close() {
108119
wsSession?.close()
109120
wsSession = null
110121
socketSubscription?.cancel()
111122
socketSubscription = null
112123
}
113124

125+
/**
126+
* Updates the target information by fetching it from the CDP.
127+
*
128+
* This is useful to refresh the target info after some operations that might change it.
129+
*/
114130
suspend fun updateTarget() {
115131
val targetInfo = target.getTargetInfo(targetId)
116132
this.targetInfo = targetInfo.targetInfo
@@ -149,11 +165,88 @@ open class Connection(
149165
}
150166
}
151167

168+
/**
169+
* Suspends the coroutine for a specified time in milliseconds.
170+
*
171+
* This is a convenience method to ensure that the target information is updated before sleeping.
172+
*
173+
* @param t Time in milliseconds to sleep.
174+
*/
152175
suspend fun sleep(t: Long) {
153176
updateTarget()
154177
delay(t)
155178
}
156179

180+
/**
181+
* Sends a CDP command and waits for the response.
182+
*
183+
* This is an alias so that you can use cdp the same way as zendriver does:
184+
* ```kotlin
185+
* // send a network.enable command with kdriver
186+
* tab.send { cdp.network.enable() }
187+
* ```
188+
* That would be equivalent to this with zendriver:
189+
* ```python
190+
* # send a network.enable command with zendriver
191+
* tab.send(cdp.network.enable())
192+
* ```
193+
*
194+
* Although you can directly call the CDP methods on the tab (recommended way of doing it):
195+
* ```kotlin
196+
* // send a network.enable command with kdriver, directly
197+
* tab.network.enable()
198+
* ```
199+
*
200+
* @param command The command to send. This is a suspending function that can call any CDP method.
201+
*
202+
* @return The result of the command, deserialized to type T.
203+
*/
204+
inline fun <T> send(command: CDP.() -> T): T {
205+
return this.command()
206+
}
207+
208+
/**
209+
* Adds a handler for a specific CDP event.
210+
*
211+
* This is an alias so that you can use cdp the same way as zendriver does:
212+
* ```kotlin
213+
* // add a handler for the consoleAPICalled event with kdriver
214+
* tab.addHandler(this, { cdp.runtime.consoleAPICalled }) { event ->
215+
* println(event)
216+
* }
217+
* ```
218+
* That would be equivalent to this with zendriver:
219+
* ```python
220+
* # add a handler for the consoleAPICalled event with zendriver
221+
* tab.add_handler(cdp.runtime.consoleAPICalled, lambda event: print(event))
222+
* ```
223+
*
224+
* Although you can directly collect the events from the tab (recommended way of doing it):
225+
* ```kotlin
226+
* // add a handler for the consoleAPICalled event with kdriver, directly
227+
* launch {
228+
* tab.runtime.consoleAPICalled.collect { event ->
229+
* println(event)
230+
* }
231+
* }
232+
* ```
233+
*
234+
* @param coroutineScope The coroutine scope in which the handler will run.
235+
* @param event A lambda that returns a Flow of the event type to listen to.
236+
* @param handler A suspend function that will be called with each event of the specified type.
237+
*
238+
* @return A Job that can be used to cancel the handler.
239+
*/
240+
inline fun <T> addHandler(
241+
coroutineScope: CoroutineScope,
242+
crossinline event: CDP.() -> Flow<T>,
243+
crossinline handler: suspend (T) -> Unit,
244+
): Job {
245+
return coroutineScope.launch {
246+
event().collect { handler(it) }
247+
}
248+
}
249+
157250
override fun toString(): String {
158251
return "Connection: ${targetInfo?.toString() ?: "no target"}"
159252
}

core/src/commonMain/kotlin/dev/kdriver/core/tab/Tab.kt

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package dev.kdriver.core.tab
22

33
import dev.kaccelero.serializers.Serialization
4-
import dev.kdriver.cdp.CDP
54
import dev.kdriver.cdp.CDPException
65
import dev.kdriver.cdp.domain.*
76
import dev.kdriver.core.browser.Browser
@@ -851,34 +850,6 @@ class Tab(
851850
// displays for a short time a red dot on the element
852851
}
853852

854-
/**
855-
* Sends a CDP command and waits for the response.
856-
*
857-
* This is an alias so that you can use cdp the same way as zendriver does:
858-
* ```kotlin
859-
* // send a network.enable command with kdriver
860-
* tab.send { network.enable() }
861-
* ```
862-
* That would be equivalent to this with zendriver:
863-
* ```python
864-
* # send a network.enable command with zendriver
865-
* tab.send(network.enable())
866-
* ```
867-
*
868-
* Although you can directly call the CDP methods on the tab (recommended way of doing it):
869-
* ```kotlin
870-
* // send a network.enable command with kdriver, directly
871-
* tab.network.enable()
872-
* ```
873-
*
874-
* @param command The command to send. This is a suspending function that can call any CDP method.
875-
*
876-
* @return The result of the command, deserialized to type T.
877-
*/
878-
inline fun <T> send(command: CDP.() -> T): T {
879-
return this.command()
880-
}
881-
882853
/**
883854
* Expects a request to match the given [urlPattern].
884855
*

core/src/jvmTest/kotlin/dev/kdriver/core/tab/TabTest.kt

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
package dev.kdriver.core.tab
22

3-
import dev.kdriver.cdp.cdp
43
import dev.kdriver.cdp.domain.Fetch
54
import dev.kdriver.cdp.domain.Network
6-
import dev.kdriver.cdp.domain.network
75
import dev.kdriver.core.browser.Browser
86
import dev.kdriver.core.exceptions.EvaluateException
97
import dev.kdriver.core.exceptions.TimeoutWaitingForElementException
@@ -15,16 +13,6 @@ import kotlin.test.*
1513

1614
class TabTest {
1715

18-
@Test
19-
fun testSend() = runBlocking {
20-
val browser = Browser.create(this, headless = true, sandbox = false)
21-
val tab = browser.mainTab ?: throw IllegalStateException("Main tab is not available")
22-
tab.send { cdp.network.setUserAgentOverride("Test user agent") }
23-
val navigatorUserAgent = tab.evaluate<String>("navigator.userAgent")
24-
assertEquals("Test user agent", navigatorUserAgent)
25-
browser.stop()
26-
}
27-
2816
@Test
2917
fun testSetUserAgentSetsNavigatorValues() = runBlocking {
3018
val browser = Browser.create(this, headless = true, sandbox = false)
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
package dev.kdriver.tutorials
2+
3+
import dev.kdriver.cdp.cdp
4+
import dev.kdriver.cdp.domain.Runtime
5+
import dev.kdriver.cdp.domain.runtime
6+
import dev.kdriver.core.browser.Browser
7+
import kotlinx.coroutines.launch
8+
import kotlinx.coroutines.runBlocking
9+
import kotlin.test.Test
10+
import kotlin.test.assertEquals
11+
12+
class CdpTest {
13+
14+
@Test
15+
fun testCdpFunctionality() = runBlocking {
16+
val browser = Browser.create(this, headless = true, sandbox = false)
17+
val page = browser.get("https://slensky.com/zendriver-examples/console.html")
18+
19+
// Those 4 lines are equivalent and do the same thing
20+
page.runtime.enable()
21+
page.cdp.runtime.enable()
22+
page.send { runtime.enable() }
23+
page.send { cdp.runtime.enable() }
24+
25+
// Create a listener for console API calls
26+
val logs = mutableListOf<String>()
27+
val consoleHandler: (Runtime.ConsoleAPICalledParameter) -> Unit = { event ->
28+
logs.add("${event.type} - ${event.args.joinToString(", ") { it.value.toString() }}")
29+
}
30+
31+
// Those 4 lines are equivalent and do the same thing
32+
val job1 = launch { page.runtime.consoleAPICalled.collect { consoleHandler(it) } }
33+
val job2 = launch { page.cdp.runtime.consoleAPICalled.collect { consoleHandler(it) } }
34+
val job3 = page.addHandler(this, { runtime.consoleAPICalled }, consoleHandler)
35+
val job4 = page.addHandler(this, { cdp.runtime.consoleAPICalled }, consoleHandler)
36+
37+
page.select("#myButton").click()
38+
39+
page.wait(1000) // Wait for the console messages to be printed
40+
41+
// Remember to cancel the job to stop listening to console events
42+
job1.cancel()
43+
job2.cancel()
44+
job3.cancel()
45+
job4.cancel()
46+
47+
assertEquals(
48+
List(4) { "log - \"Button clicked!\"" },
49+
logs
50+
)
51+
52+
browser.stop()
53+
}
54+
55+
}

0 commit comments

Comments
 (0)