Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 23 additions & 0 deletions .changeset/modernize-trigger-event.md
Original file line number Diff line number Diff line change
@@ -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.
9 changes: 7 additions & 2 deletions .github/workflows/main-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
25 changes: 24 additions & 1 deletion .github/workflows/test-headless.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
8 changes: 8 additions & 0 deletions builds/knockout/helpers/vitest-setup.js
Original file line number Diff line number Diff line change
@@ -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'

Expand Down
6 changes: 4 additions & 2 deletions builds/knockout/spec/components/defaultLoaderBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,8 @@ describe('Components: Default loader', function () {
return testTemplateFromElement('<script id="my-script-elem" type="text/html">{0}</script>', 'my-script-elem')
})

it('Can be configured as the ID of a <textarea> element', function () {
it('Can be configured as the ID of a <textarea> element', function (ctx) {
if (isHappyDom()) return ctx.skip('happy-dom: <textarea> content parsing differs')
// Special case: the textarea's value should be interpreted as a markup string
return testTemplateFromElement('<textarea id="my-textarea-elem">{0}</textarea>', 'my-textarea-elem')
})
Expand All @@ -306,7 +307,8 @@ describe('Components: Default loader', function () {
return testTemplateFromElement('<script type="text/html">{0}</script>', /* elementId */ null)
})

it('Can be configured as a <textarea> element instance', function () {
it('Can be configured as a <textarea> element instance', function (ctx) {
if (isHappyDom()) return ctx.skip('happy-dom: <textarea> content parsing differs')
// Special case: the textarea's value should be interpreted as a markup string
return testTemplateFromElement('<textarea>{0}</textarea>', /* elementId */ null)
})
Expand Down
3 changes: 2 additions & 1 deletion builds/knockout/spec/defaultBindings/attrBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ describe('Binding: Attr', function () {
expect(testNode.childNodes[0].getAttribute('second-attribute')).to.deep.equal('true')
})

it('Should be able to set namespaced attribute values', function () {
it('Should be able to set namespaced attribute values', function (ctx) {
if (isHappyDom()) return ctx.skip('happy-dom: Element.lookupNamespaceURI not implemented')
var model = { myValue: 'first value' }
testNode.innerHTML = [
'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">',
Expand Down
9 changes: 6 additions & 3 deletions builds/knockout/spec/defaultBindings/optionsBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,8 @@ describe('Binding: Options', function () {
expectHaveSelectedValues(testNode.childNodes[0], [4])
})

it('Should select caption by default and retain selection when adding multiple items', function () {
it('Should select caption by default and retain selection when adding multiple items', function (ctx) {
if (isHappyDom()) return ctx.skip('happy-dom: <select> auto-selection semantics diverge')
// This test failed in IE<=8 without changes made in #1208
testNode.innerHTML = '<select data-bind="options: filterValues, optionsCaption: \'foo\'">'
var viewModel = {
Expand All @@ -145,7 +146,8 @@ describe('Binding: Options', function () {
expect(testNode.childNodes[0].options[0]).to.equal(captionElement)
})

it('Should trigger a change event when the options selection is populated or changed by modifying the options data (single select)', function () {
it('Should trigger a change event when the options selection is populated or changed by modifying the options data (single select)', function (ctx) {
if (isHappyDom()) return ctx.skip('happy-dom: selectedIndex does not follow reordered <option>')
var observable = new ko.observableArray(['A', 'B', 'C']),
changeHandlerFireCount = 0
testNode.innerHTML = "<select data-bind='options:myValues'></select>"
Expand Down Expand Up @@ -252,7 +254,8 @@ describe('Binding: Options', function () {
expectHaveTexts(testNode.childNodes[0], ['', 'A', 'B'])
})

it('Should allow the caption to be given by an observable, and update it when the model value changes (without affecting selection)', function () {
it('Should allow the caption to be given by an observable, and update it when the model value changes (without affecting selection)', function (ctx) {
if (isHappyDom()) return ctx.skip('happy-dom: element.options[selectedIndex] can be undefined')
var myCaption = ko.observable('Initial caption')
testNode.innerHTML = '<select data-bind=\'options:["A", "B"], optionsCaption: myCaption\'></select>'
ko.applyBindings({ myCaption: myCaption }, testNode)
Expand Down
3 changes: 2 additions & 1 deletion builds/knockout/spec/defaultBindings/valueBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -406,7 +406,8 @@ describe('Binding: Value', function () {
expect(testNode.childNodes[0].selectedIndex).to.deep.equal(0)
})

it('When size > 1, should unselect all options when value is undefined, null, or \"\"', function () {
it('When size > 1, should unselect all options when value is undefined, null, or \"\"', function (ctx) {
if (isHappyDom()) return ctx.skip('happy-dom: size>1 <select> does not honor selectedIndex = -1')
var observable = new ko.observable('B')
testNode.innerHTML = '<select size=\'2\' data-bind=\'options:["A", "B"], value:myObservable\'></select>'
ko.applyBindings({ myObservable: observable }, testNode)
Expand Down
6 changes: 4 additions & 2 deletions builds/knockout/spec/onErrorBehaviors.js
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,8 @@ describe('onError handler', function () {
expect(windowOnErrorCount).to.equal(0)
})

it('fires on async component errors', async function () {
it('fires on async component errors', async function (ctx) {
if (isHappyDom()) return ctx.skip('happy-dom: setTimeout errors bypass window.onerror')
var component = {
tagName: 'test-onerror',
template: "<div data-bind='text: name'></div>",
Expand All @@ -135,7 +136,8 @@ describe('onError handler', function () {
expect(windowOnErrorCount).to.equal(1)
})

it('passes through the error instance', async function () {
it('passes through the error instance', async function (ctx) {
if (isHappyDom()) return ctx.skip('happy-dom: setTimeout errors bypass window.onerror')
var expectedInstance
ko.tasks.schedule(function () {
expectedInstance = new Error('Some error')
Expand Down
Loading
Loading