Skip to content

Commit 20132bd

Browse files
authored
Merge pull request #333 from knockout/experiment/cli-test-env
Add happy-dom test project alongside the browser matrix
2 parents e2100c7 + c87bac8 commit 20132bd

26 files changed

Lines changed: 338 additions & 225 deletions

File tree

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
---
2+
"@tko/utils": patch
3+
"@tko/binding.core": patch
4+
"@tko/provider.component": patch
5+
---
6+
7+
Modernize synthetic event construction
8+
9+
`triggerEvent` (exported from `@tko/utils`) now builds synthetic events using
10+
`new MouseEvent`/`KeyboardEvent`/`Event` constructors instead of the
11+
deprecated `document.createEvent('HTMLEvents')` + `initEvent(...)` path. This
12+
restores native side-effects in modern DOM implementations (e.g. synthetic
13+
clicks toggle checkbox `.checked` in happy-dom) without changing behavior in
14+
real browsers. `relatedTarget` is still set to the target element for mouse
15+
events to match the previous init-event argument list.
16+
17+
`@tko/binding.core` event handler no longer assigns the legacy
18+
`event.cancelBubble = true` before calling `event.stopPropagation()` — the
19+
assignment is redundant on modern events and readonly on some implementations.
20+
21+
`@tko/provider.component` now uses `Object.prototype.toString.call(node)` to
22+
detect `HTMLUnknownElement` rather than `'' + node`, which is immune to
23+
user-land `toString` overrides on custom elements.

.github/workflows/main-build.yml

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,17 @@ jobs:
3030
- name: Verify ESM extensions
3131
run: bun run verify:esm
3232

33-
- name: Run tests
34-
run: bunx vitest run
33+
- name: Run tests (browser matrix)
34+
run: bunx vitest run --project browser
3535
env:
3636
HOME: /root
3737
VITEST_BROWSERS: chromium,firefox,webkit
3838

39+
- name: Run tests (cli-happy-dom)
40+
run: bunx vitest run --project cli-happy-dom
41+
env:
42+
HOME: /root
43+
3944
- name: Upload build artifacts
4045
uses: actions/upload-artifact@v7
4146
with:

.github/workflows/test-headless.yml

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,30 @@ jobs:
3434
run: bun run verify:esm
3535

3636
- name: Run Tests
37-
run: bunx vitest run
37+
run: bunx vitest run --project browser
3838
env:
3939
HOME: /root
4040
VITEST_BROWSERS: ${{ matrix.browser }}
41+
42+
cli-happy-dom:
43+
runs-on: ubuntu-latest
44+
container:
45+
image: mcr.microsoft.com/playwright:v1.59.1-noble
46+
47+
steps:
48+
- name: Checkout code
49+
uses: actions/checkout@v6
50+
51+
- name: Install Bun
52+
run: python3 tools/install-bun
53+
54+
- name: Install dependencies
55+
run: bun install --frozen-lockfile
56+
57+
- name: Run Build
58+
run: bun run build
59+
60+
- name: Run cli-happy-dom tests
61+
run: bunx vitest run --project cli-happy-dom
62+
env:
63+
HOME: /root

builds/knockout/helpers/vitest-setup.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,19 @@
11
import * as chai from 'chai'
22
import sinon from 'sinon'
3+
import { isHappyDom } from '../../../packages/utils/helpers/test-env.ts'
34

45
// Set globals that builds/knockout specs and mocha-test-helpers.js expect
56
globalThis.chai = chai
67
globalThis.expect = chai.expect
78
globalThis.sinon = sinon
89

10+
// Test environment detector — used inside test bodies like:
11+
// it('name', function (ctx) {
12+
// if (isHappyDom()) return ctx.skip('happy-dom: reason')
13+
// // ...
14+
// })
15+
globalThis.isHappyDom = isHappyDom
16+
917
// Load the knockout build (sets globalThis.ko)
1018
import '../dist/browser.min.js'
1119

builds/knockout/spec/components/defaultLoaderBehaviors.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,8 @@ describe('Components: Default loader', function () {
283283
return testTemplateFromElement('<script id="my-script-elem" type="text/html">{0}</script>', 'my-script-elem')
284284
})
285285

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

309-
it('Can be configured as a <textarea> element instance', function () {
310+
it('Can be configured as a <textarea> element instance', function (ctx) {
311+
if (isHappyDom()) return ctx.skip('happy-dom: <textarea> content parsing differs')
310312
// Special case: the textarea's value should be interpreted as a markup string
311313
return testTemplateFromElement('<textarea>{0}</textarea>', /* elementId */ null)
312314
})

builds/knockout/spec/defaultBindings/attrBehaviors.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ describe('Binding: Attr', function () {
99
expect(testNode.childNodes[0].getAttribute('second-attribute')).to.deep.equal('true')
1010
})
1111

12-
it('Should be able to set namespaced attribute values', function () {
12+
it('Should be able to set namespaced attribute values', function (ctx) {
13+
if (isHappyDom()) return ctx.skip('happy-dom: Element.lookupNamespaceURI not implemented')
1314
var model = { myValue: 'first value' }
1415
testNode.innerHTML = [
1516
'<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">',

builds/knockout/spec/defaultBindings/optionsBehaviors.js

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,8 @@ describe('Binding: Options', function () {
127127
expectHaveSelectedValues(testNode.childNodes[0], [4])
128128
})
129129

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

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

255-
it('Should allow the caption to be given by an observable, and update it when the model value changes (without affecting selection)', function () {
257+
it('Should allow the caption to be given by an observable, and update it when the model value changes (without affecting selection)', function (ctx) {
258+
if (isHappyDom()) return ctx.skip('happy-dom: element.options[selectedIndex] can be undefined')
256259
var myCaption = ko.observable('Initial caption')
257260
testNode.innerHTML = '<select data-bind=\'options:["A", "B"], optionsCaption: myCaption\'></select>'
258261
ko.applyBindings({ myCaption: myCaption }, testNode)

builds/knockout/spec/defaultBindings/valueBehaviors.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -406,7 +406,8 @@ describe('Binding: Value', function () {
406406
expect(testNode.childNodes[0].selectedIndex).to.deep.equal(0)
407407
})
408408

409-
it('When size > 1, should unselect all options when value is undefined, null, or \"\"', function () {
409+
it('When size > 1, should unselect all options when value is undefined, null, or \"\"', function (ctx) {
410+
if (isHappyDom()) return ctx.skip('happy-dom: size>1 <select> does not honor selectedIndex = -1')
410411
var observable = new ko.observable('B')
411412
testNode.innerHTML = '<select size=\'2\' data-bind=\'options:["A", "B"], value:myObservable\'></select>'
412413
ko.applyBindings({ myObservable: observable }, testNode)

builds/knockout/spec/onErrorBehaviors.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,8 @@ describe('onError handler', function () {
110110
expect(windowOnErrorCount).to.equal(0)
111111
})
112112

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

138-
it('passes through the error instance', async function () {
139+
it('passes through the error instance', async function (ctx) {
140+
if (isHappyDom()) return ctx.skip('happy-dom: setTimeout errors bypass window.onerror')
139141
var expectedInstance
140142
ko.tasks.schedule(function () {
141143
expectedInstance = new Error('Some error')

0 commit comments

Comments
 (0)