Skip to content

Commit c58a518

Browse files
committed
Update testing docs
1 parent b47a2cc commit c58a518

4 files changed

Lines changed: 214 additions & 19 deletions

File tree

src/content/docs/docs/modules/skip-sql/index.md

Lines changed: 107 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -94,27 +94,59 @@ try sqlite.transaction {
9494

9595
### Schema Migration
9696

97-
There is no built-in support for schema migrations. Following is a part of a sample of how you might perform migrations in your own app.
97+
SkipSQL provides built-in support for version-based schema migrations using SQLite's `userVersion` pragma. The `migrate()` method accepts an array of migration closures, each representing a schema version. Only migrations that haven't been applied yet will run, each within its own transaction:
98+
99+
```swift
100+
try ctx.migrate(migrations: [
101+
// Version 1: initial schema
102+
{ ctx in
103+
try ctx.exec(sql: "CREATE TABLE DATA_ITEM (ID INTEGER PRIMARY KEY AUTOINCREMENT)")
104+
},
105+
// Version 2: add description column
106+
{ ctx in
107+
try ctx.exec(sql: "ALTER TABLE DATA_ITEM ADD COLUMN DESCRIPTION TEXT")
108+
},
109+
// Version 3: add a new table
110+
{ ctx in
111+
try ctx.createTableIfNotExists(NewTable.self)
112+
},
113+
])
114+
```
115+
116+
Each migration runs in a transaction and increments `userVersion` upon success. If a migration fails, it rolls back without affecting subsequent migrations. You can safely add new closures to the end of the array as your schema evolves — previously applied migrations are skipped.
117+
118+
#### Helper Methods
119+
120+
Additional schema utilities are available:
121+
122+
```swift
123+
// Check if a table exists in the database
124+
let exists = try ctx.tableExists("DATA_ITEM") // true
125+
126+
// Create a table from an SQLCodable type (safe if already exists)
127+
try ctx.createTableIfNotExists(DemoTable.self)
128+
129+
// Add a column to an existing table (safe if already exists)
130+
try ctx.addColumnIfNotExists(DemoTable.newColumn, to: DemoTable.table)
131+
```
132+
133+
#### Manual Migration
134+
135+
For more control, you can implement migrations manually using the `userVersion` pragma directly:
98136

99137
```swift
100-
// track the version of the schema with the `userVersion` pragma, which can be used for schema migration
101138
func migrateSchema(v version: Int64, ddl: String) throws {
102139
if ctx.userVersion < version {
103-
let startTime = Date.now
104140
try ctx.transaction {
105-
try ctx.exec(sql: ddl) // perform the DDL operation
106-
// then update the schema version
141+
try ctx.exec(sql: ddl)
107142
ctx.userVersion = version
108143
}
109-
logger.log("updated database schema to \(version) in \(startTime.durationToNow)")
110144
}
111145
}
112146

113-
// the initial creation script for a new database
114147
try migrateSchema(v: 1, ddl: """
115148
CREATE TABLE DATA_ITEM (ID INTEGER PRIMARY KEY AUTOINCREMENT)
116149
""")
117-
// migrate records to have new description column
118150
try migrateSchema(v: 2, ddl: """
119151
ALTER TABLE DATA_ITEM ADD COLUMN DESCRIPTION TEXT
120152
""")
@@ -225,6 +257,73 @@ while let row = cursor.next() {
225257
}
226258
```
227259

260+
### Convenience Query Methods
261+
262+
`SQLContext` provides several convenience methods for common operations on `SQLCodable` types:
263+
264+
#### Checking Existence
265+
266+
```swift
267+
// Check if any rows exist
268+
let hasRows = try ctx.exists(DemoTable.self)
269+
270+
// Check if rows match a predicate
271+
let hasMatch = try ctx.exists(DemoTable.self, where: DemoTable.txt.equals(SQLValue("ABC")))
272+
```
273+
274+
#### Fetching
275+
276+
```swift
277+
// Fetch all rows
278+
let all: [DemoTable] = try ctx.fetchAll(DemoTable.self)
279+
280+
// Fetch with filtering, ordering, and pagination
281+
let page: [DemoTable] = try ctx.fetchAll(DemoTable.self,
282+
where: DemoTable.num.isNotNull(),
283+
orderBy: DemoTable.int,
284+
order: .descending,
285+
limit: 10,
286+
offset: 20)
287+
```
288+
289+
#### Batch Insert
290+
291+
```swift
292+
// Insert multiple instances in a single transaction
293+
let items = [
294+
DemoTable(int: 1, txt: "A"),
295+
DemoTable(int: 2, txt: "B"),
296+
DemoTable(int: 3, txt: "C"),
297+
]
298+
let inserted = try ctx.insertAll(items) // returns instances with assigned primary keys
299+
```
300+
301+
#### Deleting All Rows
302+
303+
```swift
304+
// Delete all rows from a table
305+
try ctx.deleteAll(DemoTable.self)
306+
```
307+
308+
#### Aggregate Functions
309+
310+
```swift
311+
// Sum, average, min, max of a column
312+
let total = try ctx.sum(column: DemoTable.int, of: DemoTable.self)
313+
let average = try ctx.avg(column: DemoTable.int, of: DemoTable.self)
314+
let minimum = try ctx.min(column: DemoTable.int, of: DemoTable.self)
315+
let maximum = try ctx.max(column: DemoTable.int, of: DemoTable.self)
316+
317+
// With filtering
318+
let filteredSum = try ctx.sum(column: DemoTable.int, of: DemoTable.self,
319+
where: DemoTable.txt.isNotNull())
320+
321+
// Generic aggregate for any SQL aggregate function
322+
let result = try ctx.aggregate("GROUP_CONCAT", column: DemoTable.txt, type: DemoTable.self)
323+
```
324+
325+
All aggregate functions return an `SQLValue`, which can be accessed with `.longValue`, `.realValue`, `.textValue`, etc.
326+
228327
### Primary keys and auto-increment columns
229328

230329
SkipSQL supports primary keys that work with SQLite's ROWID mechanism,

src/content/docs/docs/modules/skip-unit/index.md

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,11 @@ By default, Skip uses the simulated Robolectric Android environment to run your
5858

5959
## Writing Tests
6060

61-
A standard Skip test is just a plain XCTest:
61+
Skip Lite supports both XCTest and a subset of Swift Testing for writing transpiled tests.
62+
63+
### XCTest
64+
65+
A standard Skip test using XCTest:
6266

6367
```swift
6468
import XCTest
@@ -70,16 +74,51 @@ final class MyUnitTests: XCTestCase {
7074
}
7175
```
7276

77+
### Swift Testing
78+
79+
You can also write tests using Swift Testing's `@Test` and `@Suite` annotations. The `#expect` macro maps to assertion functions and `#require` maps to `requireNotNil`:
80+
81+
```swift
82+
import Testing
83+
84+
@Suite struct MathTests {
85+
@Test func addition() {
86+
#expect(1 + 1 == 2)
87+
}
88+
89+
@Test func inequality() {
90+
#expect(3 != 5)
91+
}
92+
}
93+
```
94+
95+
Freestanding `@Test` functions (not inside a struct or class) are also supported and are automatically wrapped in a generated test class for JUnit:
96+
97+
```swift
98+
import Testing
99+
100+
@Test func addition() {
101+
#expect(1 + 2 == 3)
102+
}
103+
```
104+
105+
The following Swift Testing features are supported:
106+
- `@Test` functions (both as members and freestanding)
107+
- `@Suite` types
108+
- `#expect` with equality (`==`), inequality (`!=`), comparisons (`>`, `<`, `>=`, `<=`), and boolean expressions
109+
- `#require` for optional unwrapping
110+
111+
Not all Swift Testing features are currently transpiled. Parameterized tests, traits, and tags are not supported.
112+
73113
**Note**: The Skip transpiler currently does not have access to the internal API of the module being tested. If you take advantage of Swift's `@testable imports` to exercise internal API, the transpiler will not be able to perform its usual type inference when translating your test code. This just means that you might have to be more explicit about types and to fully-qualify values (e.g. `MyType.value` instead of just `.value`) when unit testing internal API.
74114

75115
## Running Tests
76116

77117
The transpiled unit tests are intended to be run as part of the standard Xcode and Swift Package Manager testing process.
78118

79-
This is done by adding one additional test class to the project's `Tests/ModuleNameTests/` folder named `XCSkipTests.swift`.
119+
The tests are driven by a test harness file called `XCSkipTests.swift`. If your test target does not already contain a file with this name, the Skip build plugin will auto-generate one during the build. This means that for most projects, you do not need to create or maintain this file yourself — just run `swift test` and the harness is created for you.
80120

81-
This additional test class is added automatically when a library is created with the `skip init` command.
82-
When adopting Skip into an existing process, add the test case manually:
121+
If you need to customize the test harness (for example, to specify a device target or adjust the Gradle task), you can add your own `XCSkipTests.swift` to your test target and the build plugin will use it instead of generating one:
83122

84123
```
85124
#if os(macOS) // Skip transpiled tests only run on macOS targets
@@ -90,14 +129,14 @@ import SkipTest
90129
final class XCSkipTests: XCTestCase, XCGradleHarness {
91130
public func testSkipModule() async throws {
92131
try await runGradleTests(device: .none) // set device ID to run in Android emulator vs. Robolectric
93-
}
94-
}
132+
}
133+
}
95134
#endif
96135
```
97136

98137
### Running Tests from Xcode
99138

100-
Once the `XCSkipTests.swift` file has been added to a project, the transpiled test cases will automatically run whenever testing against the **macOS** run destination.
139+
The transpiled test cases will automatically run whenever testing against the **macOS** run destination.
101140
As such, you need to ensure that your Swift code compiles and runs the same on macOS and iOS.
102141
This is a pre-requisite for Skip's parity testing, which runs the XCUnit test cases on macOS against the transpiled Kotlin tests in the Android testing environment. While many of the Foundation and SwiftUI APIs are identical on macOS and iOS, you may occasionally have to work around minor differences.
103142

src/content/docs/docs/modules/skip-web/index.md

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ SkipWeb provides two ways to display web content in Skip Lite apps:
2424
| **Browser chrome** | Provided by the OS (address bar, back/forward, share) | You build your own toolbar and controls |
2525
| **JavaScript access** | None — the page runs in a sandboxed browser | Full `evaluateJavaScript` support |
2626
| **Navigation control** | None — the user navigates freely within the browser | Programmatic back/forward, reload, URL changes |
27-
| **Cookie/session sharing** | Shares the user's browser cookies and autofill | Isolated web engine per `WebView` instance |
27+
| **Cookie/session sharing** | Shares the user's browser cookies and autofill | Uses the app's WebView cookie store (shared across WebViews by default; not the same store as Safari/Chrome) |
2828
| **Customization** | Custom share-sheet actions | Full layout control, scroll delegates, snapshot API |
2929

3030
Use `openWebBrowser` when you want to send the user to a web page with minimal code and maximum platform-native UX. Use `WebView` when you need to embed web content as part of your app's UI with programmatic control.
@@ -488,6 +488,58 @@ extension View {
488488
}
489489
```
490490

491+
## Cookies, Storage, and Cache
492+
493+
`SkipWeb` exposes portable browser-data APIs through `WebEngine` and `WebViewNavigator`:
494+
495+
- `cookies(for:)`
496+
- `cookieHeader(for:)`
497+
- `setCookie(_:requestURL:)`
498+
- `applySetCookieHeaders(_:for:)`
499+
- `clearCookies()`
500+
- `removeData(ofTypes:modifiedSince:)`
501+
502+
Supporting types:
503+
504+
- `WebCookie` (`name`, `value`, optional `domain`/`path`/`expires`, plus `isSecure`/`isHTTPOnly`)
505+
- `WebSiteDataType` (`cookies`, `diskCache`, `memoryCache`, `offlineWebApplicationCache`, `localStorage`, `sessionStorage`, `webSQLDatabases`, `indexedDBDatabases`)
506+
507+
Example:
508+
509+
```swift
510+
let url = URL(string: "https://example.com/path")!
511+
512+
try await navigator.setCookie(
513+
WebCookie(name: "session", value: "abc123"),
514+
requestURL: url
515+
)
516+
517+
let header = await navigator.cookieHeader(for: url)
518+
try await navigator.applySetCookieHeaders(
519+
["pref=1; Path=/; HttpOnly"],
520+
for: url
521+
)
522+
await navigator.clearCookies()
523+
try await navigator.removeData(
524+
ofTypes: Set([.diskCache, .memoryCache, .localStorage]),
525+
modifiedSince: .distantPast
526+
)
527+
```
528+
529+
Platform behavior:
530+
531+
- iOS uses the web view's `websiteDataStore.httpCookieStore`.
532+
- On iOS, cookie scope follows the `WKWebsiteDataStore` attached to that `WKWebView`.
533+
- In default SkipWeb usage, that is WebKit's shared default data store, so cookies are shared across SkipWeb web views in the app.
534+
- On iOS, a custom `WKWebView` with a different data store (for example `WKWebsiteDataStore.nonPersistent()`) uses that store instead.
535+
- Android uses `android.webkit.CookieManager`.
536+
- On Android, `CookieManager` is a process-wide singleton store shared by all `WebView` instances (not per-`WebView` configurable).
537+
- `cookies(for:)` returns URL-matching cookies; on Android this is best-effort because `CookieManager` reads as a cookie-header string (limited metadata).
538+
- `setCookie(_:requestURL:)` requires either `cookie.domain` or a `requestURL` host; otherwise it throws `WebCookieError.missingCookieDomain`.
539+
- `removeData(ofTypes:modifiedSince:)` maps to iOS `WKWebsiteDataStore.removeData`.
540+
- On Android, `removeData` requires `modifiedSince == .distantPast` when `ofTypes` is non-empty; otherwise it throws `WebDataRemovalError.unsupportedModifiedSinceOnAndroid`.
541+
- Android data removal is bucket-level (cookies/cache/storage), not timestamp-granular, and may clear a broader bucket than an individual requested data type.
542+
491543
## Contribution
492544

493545
Many delegates that are provided by `WKWebView` are not yet implemented in this project,

src/content/docs/docs/testing.md

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,9 @@ Skip provides several ways to test your code on Android, depending on whether yo
1515

1616
Skip Lite transpiles your Swift source into Kotlin, and your XCTest test cases into JUnit tests. This means your tests run as standard JUnit on the Android side, which plugs into the well-established Gradle testing infrastructure.
1717

18-
When you create a Skip Lite package with `skip init`, an `XCSkipTests.swift` file is automatically added to your test target:
18+
The transpiled tests are driven by a test harness file called `XCSkipTests.swift`. If your test target does not already contain a file with this name, the Skip build plugin will auto-generate one during the build. This means that for most projects, you do not need to create or maintain this file yourself — just run `swift test` and the harness is created for you.
19+
20+
If you need to customize the test harness (for example, to specify a device target or adjust the Gradle task), you can add your own `XCSkipTests.swift` to your test target and the build plugin will use it instead of generating one. A typical custom harness looks like this:
1921

2022
```swift
2123
#if os(macOS) // Skip transpiled tests only run on macOS targets
@@ -32,7 +34,10 @@ final class XCSkipTests: XCTestCase, XCGradleHarness {
3234

3335
This test harness is what connects Xcode to the Gradle test pipeline. When you run tests against the **macOS** destination in Xcode (or via `swift test` on the command line), `testSkipModule()` triggers the full transpilation and Gradle build, runs the JUnit tests, and reports the results back as XCTest outcomes. The net effect is that you get parity testing across both platforms from a single test run.
3436

35-
The limitation is that only XCTest-style tests are supported. Your Swift tests are transpiled into Kotlin JUnit, so they are subject to the same transpilation constraints as the rest of your Skip Lite code. Swift Testing (`@Test`) is not available in this mode.
37+
Your Swift tests are transpiled into Kotlin JUnit, so they are subject to the same transpilation constraints as the rest of your Skip Lite code. Both XCTest and a subset of Swift Testing are supported:
38+
39+
- **XCTest**: Classes inheriting from `XCTestCase` with `test`-prefixed methods are transpiled into JUnit test classes with `@Test` annotations. Standard `XCTAssert*` functions map to JUnit assertions.
40+
- **Swift Testing**: Functions annotated with `@Test` and types annotated with `@Suite` are also transpiled into JUnit tests. The `#expect` macro is mapped to assertion functions (`expectEqual`, `expectNotEqual`, `expectTrue`, `expectGreaterThan`, etc.) and `#require` is mapped to `requireNotNil`. Freestanding `@Test` functions (not inside a struct or class) are automatically wrapped in a generated test class. Not all Swift Testing features are supported — parameterized tests, traits, and tags are not currently transpiled.
3641

3742
### Local Testing with Robolectric
3843

@@ -110,7 +115,7 @@ func testAndroidSpecificFeature() {
110115
| | Skip Lite (Robolectric) | Skip Lite (Instrumented) | Skip Fuse (CLI) | Skip Fuse (APK) |
111116
|---|---|---|---|---|
112117
| **Command** | `swift test` | `ANDROID_SERIAL=… swift test` | `skip android test` | `skip android test --apk` |
113-
| **Test framework** | XCTest only (transpiled to JUnit) | XCTest only (transpiled to JUnit) | XCTest + Swift Testing | Swift Testing only |
118+
| **Test framework** | XCTest + Swift Testing (transpiled to JUnit) | XCTest + Swift Testing (transpiled to JUnit) | XCTest + Swift Testing | Swift Testing only |
114119
| **Runs on** | Host JVM (macOS/Linux) | Android emulator/device | Android emulator/device via `adb shell` | Android emulator/device as installed APK |
115120
| **Android APIs** | Simulated via Robolectric | Full | Not available (no JVM/JNI) | Full (real app process with JNI) |
116121
| **Resource bundles** | Managed by Gradle | Managed by Gradle | Yes (sidecar `.resources` dirs) | No (no Foundation APK bundle support) |

0 commit comments

Comments
 (0)