Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
7d6fab8
Merge remote-tracking branch 'origin/agent/alpha-tooling-release' int…
jderochervlk Nov 5, 2026
ac7ab70
fix(bindings): correct native API contracts
jderochervlk Nov 5, 2026
efb8e73
Merge remote-tracking branch 'origin/agent/alpha-tooling-release' int…
jderochervlk Nov 5, 2026
0c397d9
test(bindings): cover native contracts and type rejection
jderochervlk Nov 5, 2026
beda376
Merge remote-tracking branch 'origin/agent/alpha-tooling-release' int…
jderochervlk Nov 5, 2026
62f2165
test(bindings): verify remaining native behaviors
jderochervlk Nov 5, 2026
c6af120
test(bindings): exercise worker and websocket dispatch
jderochervlk Nov 7, 2026
4e20bf4
Merge remote-tracking branch 'origin/agent/alpha-tooling-release' int…
jderochervlk Nov 7, 2026
1728dc9
test(bindings): cover WebAudio and native edge cases
jderochervlk Nov 7, 2026
5ce03b4
Merge remote-tracking branch 'origin/agent/alpha-tooling-release' int…
jderochervlk Nov 7, 2026
f5df816
test(bindings): verify stream and native api edge cases
jderochervlk Nov 7, 2026
29cf9b8
Merge remote-tracking branch 'origin/agent/alpha-tooling-release' int…
jderochervlk Nov 7, 2026
5be6b71
test(bindings): harden native regression evidence
jderochervlk Nov 7, 2026
d938c71
test(bindings): isolate local browser servers
jderochervlk Nov 7, 2026
72d568e
Merge remote-tracking branch 'origin/agent/alpha-tooling-release' int…
jderochervlk Nov 7, 2026
40d9e73
test(bindings): isolate operation-specific WebCrypto results
jderochervlk Nov 7, 2026
5a5d2d6
fix(crypto): accept native algorithm parameters
jderochervlk Nov 7, 2026
6c8dfe9
Merge remote-tracking branch 'origin/agent/alpha-tooling-release' int…
jderochervlk Nov 7, 2026
f5f7d33
docs(testing): explain isolated browser ports
jderochervlk Nov 7, 2026
dd9f1c4
test(bindings): make final regression proofs discriminating
jderochervlk Nov 7, 2026
fb9d51c
Merge remote-tracking branch 'origin/agent/alpha-tooling-release' int…
jderochervlk Nov 7, 2026
14f5873
fix(fetch): require byte streams for body adapters
jderochervlk Nov 7, 2026
01aead3
test(fetch): assert every Uint8Array stream byte
jderochervlk Nov 7, 2026
0b46425
test(fetch): cover middle-byte stream mismatches
jderochervlk Nov 7, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions docs/content/docs/contributing/testing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,17 @@ Browser fixtures with asynchronous behavior must await all assertions and callba
the ReScript module finishes evaluating; the browser runner awaits module evaluation and treats
rejections and uncaught page errors as failures.

The local browser runner starts its own server on port `4173` by default and never reuses an existing
server. If another worktree or local service already uses that port, choose a free port with
`WEBAPI_TEST_PORT`; the same value configures both the test server and browser base URL:

```shell
WEBAPI_TEST_PORT=4192 npm run test:browser
```

Invalid port values fail before the runner starts, and an occupied port fails instead of attaching
to potentially stale generated files from another checkout.

Put expected-failure consumer sources under `scripts/fixtures/negative/`, outside the recursively
compiled `tests/` tree. Register each in `scripts/negative-fixtures.json` with its feature set and a
distinctive compiler diagnostic substring. The negative command must observe both compilation failure
Expand Down
32 changes: 31 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,8 @@
"prettier": "^3.8.3",
"prettier-plugin-astro": "^1.0.0",
"rescript": "13.0.0-alpha.5",
"sharp": "^0.35.3"
"sharp": "^0.35.3",
"ws": "^8.22.1"
},
"peerDependencies": {
"rescript": ">=13.0.0-alpha.5 <13.0.0"
Expand Down
11 changes: 9 additions & 2 deletions playwright.config.mjs
Original file line number Diff line number Diff line change
@@ -1,16 +1,23 @@
import { defineConfig } from "@playwright/test";

const testPort = process.env.WEBAPI_TEST_PORT ?? "4173";
if (!/^\d+$/.test(testPort) || Number(testPort) < 1 || Number(testPort) > 65535) {
throw new Error("WEBAPI_TEST_PORT must be an integer from 1 to 65535.");
}
const testOrigin = `http://127.0.0.1:${testPort}`;

export default defineConfig({
testDir: "./tests/browser",
testMatch: "**/*.spec.mjs",
workers: 1,
reporter: "list",
use: {
browserName: "chromium",
baseURL: testOrigin,
},
webServer: {
command: "node tests/browser/server.mjs",
url: "http://127.0.0.1:4173",
reuseExistingServer: !process.env.CI,
url: `${testOrigin}/tests/browser/index.html`,
reuseExistingServer: false,
},
});
5 changes: 5 additions & 0 deletions scripts/check-consumer.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,11 @@ const checkConsumers = async () => {
fixture: "scripts/consumer-safe-fetch.res",
features: ["WebAPI.SafeFetch"],
},
{
name: "Crypto",
fixture: "scripts/consumer-crypto.res",
features: ["WebAPI.Crypto"],
},
{
name: "Fetch-only SafeFetch rejection",
fixture: "scripts/consumer-safe-fetch-disabled.res",
Expand Down
78 changes: 78 additions & 0 deletions scripts/consumer-crypto.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
@scope("globalThis")
external crypto: WebAPI.Crypto.t = "crypto"

let subtle = crypto.subtle
let data =
WebAPI.SubtleCrypto.binaryDataFromUint8Array(Uint8Array.fromArray([65, 66, 67]))

let digest: ArrayBuffer.t = await subtle->WebAPI.SubtleCrypto.digest(
~algorithm=WebAPI.SubtleCrypto.algorithmObject({name: "SHA-256"}),
~data,
)
let digestBytes: Uint8Array.t = %raw(`new Uint8Array(digest)`)
assert(%raw(`digestBytes instanceof Uint8Array && digestBytes.buffer === digest`))

let hmacKey: WebAPI.SubtleCrypto.cryptoKey = await subtle->WebAPI.SubtleCrypto.generateKeySymmetric(
~hash=WebAPI.SubtleCrypto.algorithmObject({name: "SHA-256"}),
~length=256,
~extractable=true,
~keyUsages=[WebAPI.SubtleCrypto.signUsage, WebAPI.SubtleCrypto.verifyUsage],
)

let signature = await subtle->WebAPI.SubtleCrypto.sign(
~algorithm=WebAPI.SubtleCrypto.algorithmObject({name: "HMAC"}),
~key=hmacKey,
~data,
)
let verified: bool = await subtle->WebAPI.SubtleCrypto.verify(
~algorithm=WebAPI.SubtleCrypto.algorithmObject({name: "HMAC"}),
~key=hmacKey,
~signature=WebAPI.SubtleCrypto.binaryDataFromArrayBuffer(signature),
~data,
)

let rawKey: ArrayBuffer.t = await subtle->WebAPI.SubtleCrypto.exportKeyBinary(
~format=WebAPI.SubtleCrypto.Raw,
~key=hmacKey,
)
let jwk: JSON.t = await subtle->WebAPI.SubtleCrypto.exportKeyJwk(
~format=WebAPI.SubtleCrypto.Jwk,
~key=hmacKey,
)

let keyPair: WebAPI.SubtleCrypto.cryptoKeyPair = await subtle->WebAPI.SubtleCrypto.generateEcdsaKeyPair(
~namedCurve="P-256",
~extractable=true,
~keyUsages=[WebAPI.SubtleCrypto.signUsage, WebAPI.SubtleCrypto.verifyUsage],
)

let pairSignature = await subtle->WebAPI.SubtleCrypto.sign(
~algorithm=WebAPI.SubtleCrypto.algorithmObject({name: "ECDSA", hash: {name: "SHA-256"}}),
~key=keyPair.privateKey,
~data,
)

let pairVerified: bool = await subtle->WebAPI.SubtleCrypto.verify(
~algorithm=WebAPI.SubtleCrypto.algorithmObject({name: "ECDSA", hash: {name: "SHA-256"}}),
~key=keyPair.publicKey,
~signature=WebAPI.SubtleCrypto.binaryDataFromArrayBuffer(pairSignature),
~data,
)

let aesKeyData = Uint8Array.fromArray([
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15,
])->WebAPI.SubtleCrypto.binaryDataFromUint8Array
let aesKey: WebAPI.SubtleCrypto.cryptoKey = await subtle->WebAPI.SubtleCrypto.importKey(
~format=WebAPI.SubtleCrypto.rawKeyFormat,
~keyData=aesKeyData,
~algorithm=WebAPI.SubtleCrypto.algorithmObject({name: "AES-GCM", length: 128}),
~extractable=true,
~keyUsages=[WebAPI.SubtleCrypto.encryptUsage, WebAPI.SubtleCrypto.decryptUsage],
)
let aesIv = Uint8Array.fromArray([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12])
->WebAPI.SubtleCrypto.binaryDataFromUint8Array
let aesAlgorithm = WebAPI.SubtleCrypto.algorithmObject({name: "AES-GCM", iv: aesIv, tagLength: 128})
let ciphertext: ArrayBuffer.t =
await subtle->WebAPI.SubtleCrypto.encrypt(~algorithm=aesAlgorithm, ~key=aesKey, ~data)
let plaintext: ArrayBuffer.t =
await subtle->WebAPI.SubtleCrypto.decrypt(~algorithm=aesAlgorithm, ~key=aesKey, ~data=WebAPI.SubtleCrypto.binaryDataFromArrayBuffer(ciphertext))
7 changes: 7 additions & 0 deletions scripts/fixtures/negative/AnalyserRejectsOrdinaryArray.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
let context = WebAPI.OfflineAudioContext.fromChannelCountLengthAndSampleRate(
~numberOfChannels=1,
~length=128,
~sampleRate=44100.,
)
let analyser = context->WebAPI.OfflineAudioContext.createAnalyser
analyser->WebAPI.AnalyserNode.getFloatTimeDomainData([0., 0.])
2 changes: 2 additions & 0 deletions scripts/fixtures/negative/BodyInitRejectsStringStream.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
let stream: WebAPI.ReadableStream.t<string> = WebAPI.ReadableStream.make()
let _body = WebAPI.BodyInit.fromReadableStream(stream)
2 changes: 2 additions & 0 deletions scripts/fixtures/negative/BroadElementHasNoClassName.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
let broadElement: WebAPI.Element.t = %raw(`document.createElement("div")`)
let _className: string = broadElement.className
2 changes: 2 additions & 0 deletions scripts/fixtures/negative/BytesAreNotArray.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
let _bytes: array<int> =
await WebAPI.Request.fromURL("https://example.invalid/")->WebAPI.Request.bytes
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
let canvas: WebAPI.HTMLCanvasElement.t = %raw(`document.createElement("canvas")`)
WebAPI.HTMLCanvasElement.toBlob(canvas, ~callback=(_blob: WebAPI.Blob.t) => ())
2 changes: 2 additions & 0 deletions scripts/fixtures/negative/ClosestCannotNarrow.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
let element: WebAPI.Element.t = %raw(`document.createElement("div")`)
let _input: Null.t<WebAPI.HTMLInputElement.t> = element->WebAPI.Element.closest("input")
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@scope("globalThis")
external customElements: WebAPI.DOM.customElementRegistry = "customElements"

let element: WebAPI.HTMLElement.t = %raw(`document.createElement("div")`)
WebAPI.CustomElementRegistry.define(
customElements,
~name="x-invalid-binding-element",
~constructor=element,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
let _matrix = WebAPI.DOMMatrix.fromFloat32Array([1., 0., 0., 1., 0., 0.])
14 changes: 14 additions & 0 deletions scripts/fixtures/negative/GeneratePairRejectsAlgorithm.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
@scope("globalThis")
external crypto: WebAPI.Crypto.t = "crypto"

let _pair = WebAPI.SubtleCrypto.generateEcdsaKeyPair(
crypto.subtle,
~algorithm=WebAPI.SubtleCrypto.algorithmObject({
name: "HMAC",
hash: {name: "SHA-256"},
length: 256,
}),
~namedCurve="P-256",
~extractable=true,
~keyUsages=[WebAPI.SubtleCrypto.signUsage],
)
11 changes: 11 additions & 0 deletions scripts/fixtures/negative/GenerateSymmetricRejectsAlgorithm.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
@scope("globalThis")
external crypto: WebAPI.Crypto.t = "crypto"

let _key = WebAPI.SubtleCrypto.generateKeySymmetric(
crypto.subtle,
~algorithm=WebAPI.SubtleCrypto.algorithmObject({name: "ECDSA", namedCurve: "P-256"}),
~hash=WebAPI.SubtleCrypto.algorithmObject({name: "SHA-256"}),
~length=256,
~extractable=true,
~keyUsages=[WebAPI.SubtleCrypto.signUsage],
)
5 changes: 5 additions & 0 deletions scripts/fixtures/negative/IDBDeleteResultIsNotDatabase.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
@scope("globalThis")
external indexedDB: WebAPI.IndexedDbTypes.idbFactory = "indexedDB"

let request = WebAPI.IDBFactory.deleteDatabase(indexedDB, "compile-only")
let _close = request.result->WebAPI.IDBDatabase.close
15 changes: 15 additions & 0 deletions scripts/fixtures/negative/JwkIsNotBinaryKeyFormat.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
@scope("globalThis")
external crypto: WebAPI.Crypto.t = "crypto"

let key = await WebAPI.SubtleCrypto.generateKeySymmetric(
crypto.subtle,
~hash=WebAPI.SubtleCrypto.algorithmObject({name: "SHA-256"}),
~length=256,
~extractable=true,
~keyUsages=[WebAPI.SubtleCrypto.signUsage],
)
let _export = WebAPI.SubtleCrypto.exportKeyBinary(
crypto.subtle,
~format=WebAPI.SubtleCrypto.Jwk,
~key,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
let notification = WebAPI.Notification.make(~title="compile-only")
let _permission = notification.permission
15 changes: 15 additions & 0 deletions scripts/fixtures/negative/RawIsNotJwkKeyFormat.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
@scope("globalThis")
external crypto: WebAPI.Crypto.t = "crypto"

let key = await WebAPI.SubtleCrypto.generateKeySymmetric(
crypto.subtle,
~hash=WebAPI.SubtleCrypto.algorithmObject({name: "SHA-256"}),
~length=256,
~extractable=true,
~keyUsages=[WebAPI.SubtleCrypto.signUsage],
)
let _export = WebAPI.SubtleCrypto.exportKeyJwk(
crypto.subtle,
~format=WebAPI.SubtleCrypto.Raw,
~key,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
let _observer = WebAPI.ResizeObserver.make(_entries => _observer => ())
2 changes: 2 additions & 0 deletions scripts/fixtures/negative/ResponseRejectsStringStream.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
let stream: WebAPI.ReadableStream.t<string> = WebAPI.ReadableStream.make()
let _response = WebAPI.Response.fromReadableStream(stream)
5 changes: 5 additions & 0 deletions scripts/fixtures/negative/WebCryptoDigestReturnsBinary.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
let _digestReturnedJson: (
WebAPI.WebCryptoTypes.subtleCrypto,
~algorithm: WebAPI.WebCryptoTypes.algorithmIdentifier,
~data: WebAPI.ArrayBufferTypedArrayOrDataView.t,
) => promise<JSON.t> = WebAPI.SubtleCrypto.digest
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
let _exportReturnedJson: (
WebAPI.WebCryptoTypes.subtleCrypto,
~format: WebAPI.WebCryptoTypes.keyFormat,
~key: WebAPI.WebCryptoTypes.cryptoKey,
) => promise<JSON.t> = WebAPI.SubtleCrypto.exportKey
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@scope("globalThis")
external crypto: WebAPI.Crypto.t = "crypto"

let _generateKeyReturnedJson: (
WebAPI.WebCryptoTypes.subtleCrypto,
~algorithm: WebAPI.WebCryptoTypes.algorithmIdentifier,
~extractable: bool,
~keyUsages: array<WebAPI.WebCryptoTypes.keyUsage>,
) => promise<JSON.t> = WebAPI.SubtleCrypto.generateKey
9 changes: 9 additions & 0 deletions scripts/fixtures/negative/WebCryptoSignReturnsBinary.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@scope("globalThis")
external crypto: WebAPI.Crypto.t = "crypto"

let _signReturnedJson: (
WebAPI.WebCryptoTypes.subtleCrypto,
~algorithm: WebAPI.WebCryptoTypes.algorithmIdentifier,
~key: WebAPI.WebCryptoTypes.cryptoKey,
~data: WebAPI.ArrayBufferTypedArrayOrDataView.t,
) => promise<JSON.t> = WebAPI.SubtleCrypto.sign
7 changes: 7 additions & 0 deletions scripts/fixtures/negative/WebCryptoVerifyReturnsBoolean.res
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
let _verifyReturnedJson: (
WebAPI.WebCryptoTypes.subtleCrypto,
~algorithm: WebAPI.WebCryptoTypes.algorithmIdentifier,
~key: WebAPI.WebCryptoTypes.cryptoKey,
~signature: WebAPI.ArrayBufferTypedArrayOrDataView.t,
~data: WebAPI.ArrayBufferTypedArrayOrDataView.t,
) => promise<JSON.t> = WebAPI.SubtleCrypto.verify
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
@scope("globalThis")
external navigator: WebAPI.Navigator.t = "navigator"

let callback: WebAPI.WebLocksTypes.lock => promise<JSON.t> = _lock => %raw(`Promise.resolve(null)`)
let _request = navigator->WebAPI.Navigator.locks->WebAPI.LockManager.request2(
~name="compile-only",
~options={ifAvailable: true},
~callback,
)
Loading
Loading