-
Notifications
You must be signed in to change notification settings - Fork 35
Live in-browser test runner at /tests #353
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
22 commits
Select commit
Hold shift + click to select a range
11961e5
feat(docs): live browser test runner at /tests (POC, observable only)
46c3cf4
feat(docs): /tests now registers full 2669-test suite
f0f0a0c
feat(docs): split test bundle, add version-switcher UI
407d59f
tests.astro: swap unpkg → jsdelivr for CDN-loaded scripts
587833d
docs: correct note — TKO does ship ESM
a81a7f7
fix(tests): single-module graph for source bundle
886457e
tests UI: restrict to build-mode, hide dev/source toggle
98ca441
tests: hygiene work from state-leak investigation
09806fa
feat(tests): iframe-per-spec runner with TKO-driven UI
54fdc4a
tests: drop remaining iframe-runner failures from 48 toward ~1
4fd954d
feat(tests): green source-mode suite (2708/0/42 in 11.9s)
f2a8f54
tests: parallel hidden + serial focus queues (simplify fixes)
585c328
tests: link /tests from top nav
0b5648d
tests: apply PR review (Codex/Copilot/CodeRabbit)
e7184b6
tests UI: fixed-bottom work area
0771ac8
tests UI: floating translucent work-area + brand polish
aa98b20
tests UI: TKO wordmark match landing brand (24px/600)
92c3fe8
tests UI: TKO wordmark uses Lobster to match landing
a66121f
tests: address CodeRabbit/Copilot PR follow-ups + dark-mode TKO
c5bd8d7
tests: tighten browser-setup.js comments
f68cc67
docs: add browser test runner plan + promote plans/ in AGENTS
a7b0a5f
docs: tighten AGENTS.md Plans + Before-you-start sections
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,117 @@ | ||
| // Setup for running TKO specs in a real browser under Mocha. | ||
| // Counterpart to vitest-setup.js. Loaded as the first import of | ||
| // the test bundle (tko.io/scripts/bundle-tests.mjs). | ||
| // | ||
| // Assumes `mocha.setup('bdd')` already ran (so `before` / `after` | ||
| // / `beforeEach` / `afterEach` are global) and `globalThis.ko` | ||
| // was set by the bundled IIFE in /tests/source/setup.js, which | ||
| // this module is imported from. | ||
|
|
||
| import * as chai from 'chai' | ||
| import sinon from 'sinon' | ||
| // Register punctuation filters on shared `@tko/utils` options so | ||
| // specs that construct Parsers directly (`new Parser().parse('x | tail')`) | ||
| // can resolve them. The builder registers the same filters at page | ||
| // startup but on a module-local options reference — not this one. | ||
| import { filters as punctuationFilters } from '@tko/filter.punches' | ||
| import { options as sharedOptions } from '@tko/utils' | ||
|
|
||
| globalThis.chai = chai | ||
| globalThis.expect = chai.expect | ||
| globalThis.sinon = sinon | ||
| globalThis.isHappyDom = () => false | ||
|
|
||
| sharedOptions.filters = Object.assign(sharedOptions.filters || {}, punctuationFilters) | ||
|
|
||
| // mocha-test-helpers wires root beforeEach/afterEach hooks, so it | ||
| // must be imported after `mocha.setup('bdd')` ran. | ||
| import './mocha-test-helpers.js' | ||
|
|
||
| // Disable the 25ms JSX cleanup timer so it can't race test teardown. | ||
| before(() => { | ||
| if (globalThis.ko?.options) { | ||
| globalThis.ko.options.jsxCleanBatchSize = 0 | ||
| } | ||
| }) | ||
|
|
||
| // Iframe focus-event polyfill. | ||
| // | ||
| // Chromium refuses to grant programmatic `iframe.contentWindow.focus()` | ||
| // true system focus from a parent that already holds focus — the | ||
| // iframe never passes `document.hasFocus() === true`, so `focusin` | ||
| // / `focusout` are suppressed when specs call `element.focus()` | ||
| // inside. The `hasfocus` binding observes those events (not | ||
| // `document.activeElement`), so without this patch those specs | ||
| // fail under both Playwright and a real Chrome tab. | ||
| // | ||
| // Wrap `focus`/`blur` to dispatch the missing events synchronously | ||
| // after the native call. If the browser DOES regain system focus | ||
| // and fires them too, observers see duplicates — harmless for | ||
| // these specs (state-checking, not call-count). | ||
| // | ||
| // Scope-guarded to iframes (`window.parent !== window`) so the | ||
| // parent page is never patched. | ||
| // | ||
| // Refs: https://github.com/jsdom/jsdom/pull/2996 (same shape as | ||
| // this wrap), https://html.spec.whatwg.org/multipage/interaction.html#focusing-elements. | ||
| if (window.parent !== window && !HTMLElement.prototype.__tkoFocusPatched) { | ||
| const HE = HTMLElement.prototype | ||
| HE.__tkoFocusPatched = true | ||
| const origFocus = HE.focus | ||
| const origBlur = HE.blur | ||
| HE.focus = function (...args) { | ||
| const wasActive = this.ownerDocument.activeElement | ||
| origFocus.apply(this, args) | ||
| if (this.ownerDocument.activeElement === this && wasActive !== this) { | ||
| this.dispatchEvent(new FocusEvent('focus', { bubbles: false, relatedTarget: wasActive })) | ||
| this.dispatchEvent(new FocusEvent('focusin', { bubbles: true, relatedTarget: wasActive })) | ||
| } | ||
| } | ||
| HE.blur = function (...args) { | ||
| const wasActive = this.ownerDocument.activeElement | ||
| origBlur.apply(this, args) | ||
| if (wasActive === this && this.ownerDocument.activeElement !== this) { | ||
| this.dispatchEvent(new FocusEvent('blur', { bubbles: false, relatedTarget: this.ownerDocument.activeElement })) | ||
| this.dispatchEvent(new FocusEvent('focusout', { bubbles: true, relatedTarget: this.ownerDocument.activeElement })) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| // Unscoped sinon fakes (`sinon.spy(obj,'m')`, `sinon.useFakeTimers()`) | ||
| // leak across specs if not restored, producing bogus call-count | ||
| // diffs or "Can't install fake timers twice". `sinon.restore()` is | ||
| // a no-op for sandbox-scoped fakes. Vitest isolates per-file so | ||
| // doesn't need this hook. | ||
| afterEach(() => { | ||
| if (globalThis.sinon?.restore) globalThis.sinon.restore() | ||
| }) | ||
|
|
||
| // Vitest-style context-arg shim. | ||
| // | ||
| // Specs written `function (ctx) { if (isHappyDom()) return ctx.skip(…) }` | ||
| // look like Mocha done-callback specs (`fn.length === 1`) and time | ||
| // out after ~10s because they never call done. Wrap `it` to detect | ||
| // the ctx shape (uses `.skip(...)` and never calls `done(`) and | ||
| // invoke with a synthetic `{ skip }` while hiding arity from Mocha. | ||
| { | ||
| const wrap = orig => | ||
| function (name, fn) { | ||
| if (typeof fn === 'function' && fn.length === 1) { | ||
| const src = fn.toString() | ||
| const ctxStyle = /\.skip\s*\(/.test(src) && !/\bdone\s*\(/.test(src) | ||
| if (ctxStyle) { | ||
| const wrapped = function () { | ||
| return fn.call(this, { skip: reason => this.skip(reason) }) | ||
| } | ||
| Object.defineProperty(wrapped, 'length', { value: 0 }) | ||
| return orig.call(this, name, wrapped) | ||
| } | ||
| } | ||
| return orig.apply(this, arguments) | ||
| } | ||
| const origIt = globalThis.it | ||
| const wrappedIt = wrap(origIt) | ||
| wrappedIt.only = wrap(origIt.only) | ||
| wrappedIt.skip = origIt.skip | ||
| globalThis.it = wrappedIt | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.