diff --git a/.changeset/modernize-trigger-event.md b/.changeset/modernize-trigger-event.md
new file mode 100644
index 00000000..594fb790
--- /dev/null
+++ b/.changeset/modernize-trigger-event.md
@@ -0,0 +1,23 @@
+---
+"@tko/utils": patch
+"@tko/binding.core": patch
+"@tko/provider.component": patch
+---
+
+Modernize synthetic event construction
+
+`triggerEvent` (exported from `@tko/utils`) now builds synthetic events using
+`new MouseEvent`/`KeyboardEvent`/`Event` constructors instead of the
+deprecated `document.createEvent('HTMLEvents')` + `initEvent(...)` path. This
+restores native side-effects in modern DOM implementations (e.g. synthetic
+clicks toggle checkbox `.checked` in happy-dom) without changing behavior in
+real browsers. `relatedTarget` is still set to the target element for mouse
+events to match the previous init-event argument list.
+
+`@tko/binding.core` event handler no longer assigns the legacy
+`event.cancelBubble = true` before calling `event.stopPropagation()` — the
+assignment is redundant on modern events and readonly on some implementations.
+
+`@tko/provider.component` now uses `Object.prototype.toString.call(node)` to
+detect `HTMLUnknownElement` rather than `'' + node`, which is immune to
+user-land `toString` overrides on custom elements.
diff --git a/.github/workflows/main-build.yml b/.github/workflows/main-build.yml
index 62f1b1f7..25ae8a36 100644
--- a/.github/workflows/main-build.yml
+++ b/.github/workflows/main-build.yml
@@ -30,12 +30,17 @@ jobs:
- name: Verify ESM extensions
run: bun run verify:esm
- - name: Run tests
- run: bunx vitest run
+ - name: Run tests (browser matrix)
+ run: bunx vitest run --project browser
env:
HOME: /root
VITEST_BROWSERS: chromium,firefox,webkit
+ - name: Run tests (cli-happy-dom)
+ run: bunx vitest run --project cli-happy-dom
+ env:
+ HOME: /root
+
- name: Upload build artifacts
uses: actions/upload-artifact@v7
with:
diff --git a/.github/workflows/test-headless.yml b/.github/workflows/test-headless.yml
index 89fb725d..cce9cafa 100644
--- a/.github/workflows/test-headless.yml
+++ b/.github/workflows/test-headless.yml
@@ -34,7 +34,30 @@ jobs:
run: bun run verify:esm
- name: Run Tests
- run: bunx vitest run
+ run: bunx vitest run --project browser
env:
HOME: /root
VITEST_BROWSERS: ${{ matrix.browser }}
+
+ cli-happy-dom:
+ runs-on: ubuntu-latest
+ container:
+ image: mcr.microsoft.com/playwright:v1.59.1-noble
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v6
+
+ - name: Install Bun
+ run: python3 tools/install-bun
+
+ - name: Install dependencies
+ run: bun install --frozen-lockfile
+
+ - name: Run Build
+ run: bun run build
+
+ - name: Run cli-happy-dom tests
+ run: bunx vitest run --project cli-happy-dom
+ env:
+ HOME: /root
diff --git a/builds/knockout/helpers/vitest-setup.js b/builds/knockout/helpers/vitest-setup.js
index 09fc6c89..2b2557d4 100644
--- a/builds/knockout/helpers/vitest-setup.js
+++ b/builds/knockout/helpers/vitest-setup.js
@@ -1,11 +1,19 @@
import * as chai from 'chai'
import sinon from 'sinon'
+import { isHappyDom } from '../../../packages/utils/helpers/test-env.ts'
// Set globals that builds/knockout specs and mocha-test-helpers.js expect
globalThis.chai = chai
globalThis.expect = chai.expect
globalThis.sinon = sinon
+// Test environment detector — used inside test bodies like:
+// it('name', function (ctx) {
+// if (isHappyDom()) return ctx.skip('happy-dom: reason')
+// // ...
+// })
+globalThis.isHappyDom = isHappyDom
+
// Load the knockout build (sets globalThis.ko)
import '../dist/browser.min.js'
diff --git a/builds/knockout/spec/components/defaultLoaderBehaviors.js b/builds/knockout/spec/components/defaultLoaderBehaviors.js
index d5f896dd..25eb57ea 100644
--- a/builds/knockout/spec/components/defaultLoaderBehaviors.js
+++ b/builds/knockout/spec/components/defaultLoaderBehaviors.js
@@ -283,7 +283,8 @@ describe('Components: Default loader', function () {
return testTemplateFromElement('', 'my-script-elem')
})
- it('Can be configured as the ID of a