describe("Core htmx internals Tests", function() {

    beforeEach(function () {
        this.server = makeServer();
        clearWorkArea();
    });
    afterEach(function () {
        this.server.restore();
        clearWorkArea();
    });

    it("makeFragment works with janky stuff", function(){
        htmx._("makeFragment")("").tagName.should.equal("BODY");
        htmx._("makeFragment")("").tagName.should.equal("BODY");

        //NB - the tag name should be the *parent* element hosting the HTML since we use the fragment children
        // for the swap
        htmx._("makeFragment")("	").tagName.should.equal("TR");
        htmx._("makeFragment")("

").tagName.should.equal("TABLE");
        htmx._("makeFragment")("<col></col>").tagName.should.equal("COLGROUP");
        htmx._("makeFragment")("

").tagName.should.equal("TBODY");
    })

    it("makeFragment works with template wrapping", function(){
        if (!supportsTemplates()) {
            this._runnable.title += " - Skipped as IE11 doesn't support templates"
            this.skip()
            return
        }
        try {
            htmx._("makeFragment")("").children.length.should.equal(0);
            htmx._("makeFragment")("").children.length.should.equal(0);

            var fragment = htmx._("makeFragment")("	");
            fragment.firstElementChild.tagName.should.equal("TD");

            fragment = htmx._("makeFragment")("

");
            fragment.firstElementChild.tagName.should.equal("THEAD");

            fragment = htmx._("makeFragment")("<col></col>");
            fragment.firstElementChild.tagName.should.equal("COL");

            fragment = htmx._("makeFragment")("

");
            fragment.firstElementChild.tagName.should.equal("TR");

        } finally {
            htmx.config.useTemplateFragments = false;
        }
    })

    it("makeFragment works with template wrapping and funky combos", function(){
        if (!supportsTemplates()) {
            this._runnable.title += " - Skipped as IE11 doesn't support templates"
            this.skip()
            return
        }
        htmx.config.useTemplateFragments = true;
        try {
            var fragment = htmx._("makeFragment")("

");
            fragment.children[0].tagName.should.equal("TD");
            fragment.children[1].tagName.should.equal("DIV");
        } finally {
            htmx.config.useTemplateFragments = false;
        }
    })

    it("set header works with non-ASCII values", function(){
        var xhr = new XMLHttpRequest();
        xhr.open("GET", "/dummy");
        htmx._("safelySetHeaderValue")(xhr, "Example", "привет");
        // unfortunately I can't test the value :/
        // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest
    })

    it("handles parseInterval correctly", function() {
        chai.expect(htmx.parseInterval("1ms")).to.be.equal(1);
        chai.expect(htmx.parseInterval("300ms")).to.be.equal(300);
        chai.expect(htmx.parseInterval("1s")).to.be.equal(1000)
        chai.expect(htmx.parseInterval("1.5s")).to.be.equal(1500)
        chai.expect(htmx.parseInterval("2s")).to.be.equal(2000)

        chai.expect(htmx.parseInterval(null)).to.be.undefined
        chai.expect(htmx.parseInterval("")).to.be.undefined
        chai.expect(htmx.parseInterval("undefined")).to.be.undefined
        chai.expect(htmx.parseInterval("true")).to.be.undefined
        chai.expect(htmx.parseInterval("false")).to.be.undefined
    })

    it("tokenizes correctly", function() {
        chai.expect(htmx._("tokenizeString")("a,")).to.be.deep.equal(['a', ',']);
        chai.expect(htmx._("tokenizeString")("aa,")).to.be.deep.equal(['aa', ',']);
        chai.expect(htmx._("tokenizeString")("aa,aa")).to.be.deep.equal(['aa', ',', 'aa']);
        chai.expect(htmx._("tokenizeString")("aa.aa")).to.be.deep.equal(['aa', '.', 'aa']);
    })

    it("tags respond correctly to shouldCancel", function() {
        var anchorThatShouldCancel = make("");
        htmx._("shouldCancel")({type:'click'}, anchorThatShouldCancel).should.equal(true);

        var anchorThatShouldCancel = make("");
        htmx._("shouldCancel")({type:'click'}, anchorThatShouldCancel).should.equal(true);

        var anchorThatShouldNotCancel = make("");
        htmx._("shouldCancel")({type:'click'}, anchorThatShouldNotCancel).should.equal(false);

        var form = make("<form></form>");
        htmx._("shouldCancel")({type:'submit'}, form).should.equal(true);

        var form = make("<form><input id='i1' type='submit'></form>");
        var input = byId("i1");
        htmx._("shouldCancel")({type:'click'}, input).should.equal(true);

        var form = make("<form><button id='b1' type='submit'></form>");
        var button = byId("b1");
        htmx._("shouldCancel")({type:'click'}, button).should.equal(true);

    })

    it("unset properly unsets a given attribute", function(){
        make("

");
        var div = byId("d1");
        should.equal(undefined, htmx._("getClosestAttributeValue")(div, "foo"));
    })

    it("unset properly unsets a given attribute on a parent", function(){
        make("

");
        var div = byId("d1");
        should.equal(undefined, htmx._("getClosestAttributeValue")(div, "foo"));
    })

    it("unset does not unset a value below it in the hierarchy", function(){
        make("

");
        var div = byId("d1");
        should.equal("2", htmx._("getClosestAttributeValue")(div, "foo"));
    })

    it("encoding values respects enctype on forms", function(){
        var form = make("<form enctype='multipart/form-data'></form>");
        var value = htmx._("encodeParamsForBody")(null, form, {});
        (value instanceof FormData).should.equal(true);
    })

});


Htmx includes a WebSockets test server with many more examples of how to use Server Sent Events.  Download the htmx source code from GitHub and navigate to /test/servers/ws to experiment.

keep keep keep keep keep replace keep keep keep keep

Htmx includes a WebSockets test server with many more examples of how to use Server Sent Events.  Download the htmx source code from GitHub and navigate to /test/ws-sse to experiment.

<!DOCTYPE html>

    <meta charset="UTF-8">
    <title>Aborting Request Event Test</title>

Functionality

Aborting Requests

Confirm & Prompt

CSP Test

Form Test

Intersect Test

Indicator CSS Test

Indicator CSS Test 2

Poll Conditionals

Polling Cancels On Reprocessing

Preload Test

Preload Fragment Test

Head Merge Extension Tests

Cache Busting Test

Scroll Tests

Scroll Event Handler

Start/End

Targets

History

Core History Test

History Refresh Test

Restored Test

Navigate, Refresh, Back Regression

Anchors

History Style

Perf

Manual Perf Test


If the SSE Event Stream is closed unexpectedly, browsers are supposed to attempt to reconnect automatically.  However, in rare situations this does not work and your browser can be left hanging.  This extension adds its own reconnection logic (using an [exponential-backoff algorithm](https://en.wikipedia.org/wiki/Exponential_backoff)) on top of the browser's automatic reconnection, so that your SSE streams will always be as reliable as possible.

### Testing SSE Connections with the Demo Server

Htmx includes a demo SSE server written in Go that will help you to see SSE in action, and begin bootstrapping your own SSE code.  It is located in the /test/servers/sse folder of the htmx distribution.  Look at /test/servers/ws/README.md for instructions on running and using the test server.

Previous versions of htmx used a built-in tag `hx-sse` to implement Server Sent Events.  This code has been migrated into an extension instead.  Here are the steps you need to take to migrate to this version:

keep keep keep keep replace keep keep keep keep keep

Htmx includes a demo SSE server written in Node.js that will help you to see SSE in action, and begin bootstrapping your own SSE code.  It is located in the /test/ws-sse folder of the htmx distribution.  Look at /test/ws-sse/README.md for instructions on running and using the test server.

+++
title = "hx-ws"
+++

*Note: This attribute will be migrated to an extension in htmx 2.0, which is available now.  Please visit the
[WebSockets extension page](@/extensions/web-sockets.md) to learn about the new implementation of Web Sockets as an extension.*

The `hx-ws` allows you to work with [Web Sockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications)
directly from HTML.  The value of the attribute can be one or more of the following, separated by commas:

* `connect:<url>` or `connect:<prefix>:<url>` - A URL to establish an `WebSocket` connection against.
* Prefixes `ws` or `wss` can optionally be specified. If not specified, HTMX defaults to add the location's scheme-type, host and port to have browsers send cookies via websockets.
* `send` - Sends a message to the nearest websocket based on the trigger value for the element (either the natural event
or the event specified by [`hx-trigger`])

Here is an example:

```html
  <div hx-ws="connect:/chatroom">
    <div id="chat_room">
      ...
    </div>
    <form hx-ws="send">
        <input name="chat_message">
    </form>
  </div>
```

This example establishes a WebSocket to the `chatroom` end point.  Content that is sent down from the websocket will
be parsed as HTML and swapped in by the `id` property, using the same logic as [Out of Band Swaps](@/attributes/hx-swap-oob.md).

The form uses the `send` syntax to indicate that when it is submitted, the form values should be serialized as JSON
and send to the nearest enclosing `WebSocket`.

The serialized values will include a field, `HEADERS`, that includes the headers normally submitted with an htmx
request.

After an unexpected connection loss due to `Abnormal Closure`, `Service Restart` or `Try Again Later`,
reconnecting is tried until successful.
The default reconnection interval is implemented with the full-jitter exponential-backoff algorithm.
Own implementations can be provided by setting `htmx.config.wsReconnectDelay` to a function with
`retryCount` as its only parameter.

### Test Web Sockets Server

Htmx includes a WebSockets test server with many more examples of how to use Server Sent Events.  Download the htmx source code from GitHub and navigate to /test/servers/ws to experiment.

## Notes

* `hx-ws` is not inherited


{% extends "base.html" %}

{% block content %}
    {{ section.content | safe }}
{% endblock %}


htmx.defineExtension('ajax-header', {
    onEvent: function (name, evt) {
        if (name === "htmx:configRequest") {
            evt.detail.headers['X-Requested-With'] = 'XMLHttpRequest';
        }
    }
});


// https://htmx.org/reference/#api

/**
 * This method adds a class to the given element.
 *
 * https://htmx.org/api/#addClass
 *
 * @param elt the element to add the class to
 * @param clazz the class to add
 * @param delay the delay (in milliseconds before class is added)
 */
export function addClass(elt: Element, clazz: string, delay?: number): void;

/**
 * Issues an htmx-style AJAX request
 *
 * https://htmx.org/api/#ajax
 *
 * @param verb 'GET', 'POST', etc.
 * @param path the URL path to make the AJAX
 * @param element the element to target (defaults to the **body**)
 * @returns Promise that resolves immediately if no request is sent, or when the request is complete
 */
export function ajax(verb: string, path: string, element: Element): Promise<void>;

/**
 * Issues an htmx-style AJAX request
 *
 * https://htmx.org/api/#ajax
 *
 * @param verb 'GET', 'POST', etc.
 * @param path the URL path to make the AJAX
 * @param selector a selector for the target
 * @returns Promise that resolves immediately if no request is sent, or when the request is complete
 */
export function ajax(verb: string, path: string, selector: string): Promise<void>;

/**
 * Issues an htmx-style AJAX request
 *
 * https://htmx.org/api/#ajax
 *
 * @param verb 'GET', 'POST', etc.
 * @param path the URL path to make the AJAX
 * @param context a context object that contains any of the following
 * @returns Promise that resolves immediately if no request is sent, or when the request is complete
 */
export function ajax(
    verb: string,
    path: string,
    context: Partial<{ source: any; event: any; handler: any; target: any; swap: any; values: any; headers: any }>
): Promise<void>;

/**
 * Finds the closest matching element in the given elements parentage, inclusive of the element
 *
 * https://htmx.org/api/#closest
 *
 * @param elt the element to find the selector from
 * @param selector the selector to find
 */
export function closest(elt: Element, selector: string): Element | null;

/**
 * A property holding the configuration htmx uses at runtime.
 *
 * Note that using a [meta tag](https://htmx.org/docs/#config) is the preferred mechanism for setting these properties.
 *
 * https://htmx.org/api/#config
 */
export var config: HtmxConfig;

/**
 * A property used to create new [Server Sent Event](https://htmx.org/docs/#sse) sources. This can be updated to provide custom SSE setup.
 *
 * https://htmx.org/api/#createEventSource
 */
export var createEventSource: (url: string) => EventSource;

/**
 * A property used to create new [WebSocket](https://htmx.org/docs/#websockets). This can be updated to provide custom WebSocket setup.
 *
 * https://htmx.org/api/#createWebSocket
 */
export var createWebSocket: (url: string) => WebSocket;

/**
 * Defines a new htmx [extension](https://htmx.org/extensions).
 *
 * https://htmx.org/api/#defineExtension
 *
 * @param name the extension name
 * @param ext the extension definition
 */
export function defineExtension(name: string, ext: HtmxExtension): void;

/**
 * Finds an element matching the selector
 *
 * https://htmx.org/api/#find
 *
 * @param selector the selector to match
 */
export function find(selector: string): Element | null;

/**
 * Finds an element matching the selector
 *
 * https://htmx.org/api/#find
 *
 * @param elt the root element to find the matching element in, inclusive
 * @param selector the selector to match
 */
export function find(elt: Element, selector: string): Element | null;

/**
 * Finds all elements matching the selector
 *
 * https://htmx.org/api/#findAll
 *
 * @param selector the selector to match
 */
export function findAll(selector: string): NodeListOf<Element>;

/**
 * Finds all elements matching the selector
 *
 * https://htmx.org/api/#findAll
 *
 * @param elt the root element to find the matching elements in, inclusive
 * @param selector the selector to match
 */
export function findAll(elt: Element, selector: string): NodeListOf<Element>;

/**
 * Log all htmx events, useful for debugging.
 *
 * https://htmx.org/api/#logAll
 */
export function logAll(): void;

/**
 * The logger htmx uses to log with
 *
 * https://htmx.org/api/#logger
 */
export var logger: (elt: Element, eventName: string, detail: any) => void | null;

/**
 * Removes an event listener from an element
 *
 * https://htmx.org/api/#off
 *
 * @param eventName the event name to remove the listener from
 * @param listener the listener to remove
 */
export function off(eventName: string, listener: (evt: Event) => void): (evt: Event) => void;

/**
 * Removes an event listener from an element
 *
 * https://htmx.org/api/#off
 *
 * @param target the element to remove the listener from
 * @param eventName the event name to remove the listener from
 * @param listener the listener to remove
 */
export function off(target: string, eventName: string, listener: (evt: Event) => void): (evt: Event) => void;

/**
 * Adds an event listener to an element
 *
 * https://htmx.org/api/#on
 *
 * @param eventName the event name to add the listener for
 * @param listener the listener to add
 */
export function on(eventName: string, listener: (evt: Event) => void): (evt: Event) => void;

/**
 * Adds an event listener to an element
 *
 * https://htmx.org/api/#on
 *
 * @param target the element to add the listener to
 * @param eventName the event name to add the listener for
 * @param listener the listener to add
 */
export function on(target: string, eventName: string, listener: (evt: Event) => void): (evt: Event) => void;

/**
 * Adds a callback for the **htmx:load** event. This can be used to process new content, for example initializing the content with a javascript library
 *
 * https://htmx.org/api/#onLoad
 *
 * @param callback the callback to call on newly loaded content
 */
export function onLoad(callback: (element: Element) => void): void;

/**
 * Parses an interval string consistent with the way htmx does. Useful for plugins that have timing-related attributes.
 *
 * Caution: Accepts an int followed by either **s** or **ms**. All other values use **parseFloat**
 *
 * https://htmx.org/api/#parseInterval
 *
 * @param str timing string
 */
export function parseInterval(str: string): number;

/**
 * Processes new content, enabling htmx behavior. This can be useful if you have content that is added to the DOM outside of the normal htmx request cycle but still want htmx attributes to work.
 *
 * https://htmx.org/api/#process
 *
 * @param element element to process
 */
export function process(element: Element): void;

/**
 * Removes an element from the DOM
 *
 * https://htmx.org/api/#remove
 *
 * @param elt element to remove
 * @param delay the delay (in milliseconds before element is removed)
 */
export function remove(elt: Element, delay?: number): void;

/**
 * Removes a class from the given element
 *
 * https://htmx.org/api/#removeClass
 *
 * @param elt element to remove the class from
 * @param clazz the class to remove
 * @param delay the delay (in milliseconds before class is removed)
 */
export function removeClass(elt: Element, clazz: string, delay?: number): void;

/**
 * Removes the given extension from htmx
 *
 * https://htmx.org/api/#removeExtension
 *
 * @param name the name of the extension to remove
 */
export function removeExtension(name: string): void;

/**
 * Takes the given class from its siblings, so that among its siblings, only the given element will have the class.
 *
 * https://htmx.org/api/#takeClass
 *
 * @param elt the element that will take the class
 * @param clazz the class to take
 */
export function takeClass(elt: Element, clazz: string): void;

/**
 * Toggles the given class on an element
 *
 * https://htmx.org/api/#toggleClass
 *
 * @param elt the element to toggle the class on
 * @param clazz the class to toggle
 */
export function toggleClass(elt: Element, clazz: string): void;

/**
 * Triggers a given event on an element
 *
 * https://htmx.org/api/#trigger
 *
 * @param elt the element to trigger the event on
 * @param name the name of the event to trigger
 * @param detail details for the event
 */
export function trigger(elt: Element, name: string, detail: any): void;

/**
 * Returns the input values that would resolve for a given element via the htmx value resolution mechanism
 *
 * https://htmx.org/api/#values
 *
 * @param elt the element to resolve values on
 * @param requestType the request type (e.g. **get** or **post**) non-GET's will include the enclosing form of the element. Defaults to **post**
 */
export function values(elt: Element, requestType?: string): any;

export const version: string;

export interface HtmxConfig {
    /**
     * The attributes to settle during the settling phase.
     * @default ["class", "style", "width", "height"]
     */
    attributesToSettle?: ["class", "style", "width", "height"] | string[];
    /**
     * If the focused element should be scrolled into view.
     * @default false
    */
    defaultFocusScroll?: boolean;
    /**
     * The default delay between completing the content swap and settling attributes.
     * @default 20
     */
    defaultSettleDelay?: number;
    /**
     * The default delay between receiving a response from the server and doing the swap.
     * @default 0
     */
    defaultSwapDelay?: number;
    /**
     * The default swap style to use if **[hx-swap](https://htmx.org/attributes/hx-swap)** is omitted.
     * @default "innerHTML"
     */
    defaultSwapStyle?: "innerHTML" | string;
    /**
     * The number of pages to keep in **localStorage** for history support.
     * @default 10
     */
    historyCacheSize?: number;
    /**
     * Whether or not to use history.
     * @default true
     */
    historyEnabled?: boolean;
    /**
     * If true, htmx will inject a small amount of CSS into the page to make indicators invisible unless the **htmx-indicator** class is present.
     * @default true
     */
    includeIndicatorStyles?: boolean;
    /**
     * The class to place on indicators when a request is in flight.
     * @default "htmx-indicator"
     */
    indicatorClass?: "htmx-indicator" | string;
    /**
     * The class to place on triggering elements when a request is in flight.
     * @default "htmx-request"
     */
    requestClass?: "htmx-request" | string;
    /**
     * The class to temporarily place on elements that htmx has added to the DOM.
     * @default "htmx-added"
     */
    addedClass?: "htmx-added" | string;
    /**
     * The class to place on target elements when htmx is in the settling phase.
     * @default "htmx-settling"
     */
    settlingClass?: "htmx-settling" | string;
    /**
     * The class to place on target elements when htmx is in the swapping phase.
     * @default "htmx-swapping"
     */
    swappingClass?: "htmx-swapping" | string;
    /**
     * Allows the use of eval-like functionality in htmx, to enable **hx-vars**, trigger conditions & script tag evaluation. Can be set to **false** for CSP compatibility.
     * @default true
     */
    allowEval?: boolean;
    /**
     * Use HTML template tags for parsing content from the server. This allows you to use Out of Band content when returning things like table rows, but it is *not* IE11 compatible.
     * @default false
     */
    useTemplateFragments?: boolean;
    /**
     * Allow cross-site Access-Control requests using credentials such as cookies, authorization headers or TLS client certificates.
     * @default false
     */
    withCredentials?: boolean;
    /**
     * The default implementation of **getWebSocketReconnectDelay** for reconnecting after unexpected connection loss by the event code **Abnormal Closure**, **Service Restart** or **Try Again Later**.
     * @default "full-jitter"
     */
    wsReconnectDelay?: "full-jitter" | string | ((retryCount: number) => number);
    // following don't appear in the docs
    /** @default false */
    refreshOnHistoryMiss?: boolean;
    /** @default 0 */
    timeout?: number;
    /** @default "[hx-disable], [data-hx-disable]" */
    disableSelector?: "[hx-disable], [data-hx-disable]" | string;
    /** @default "smooth" */
    scrollBehavior?: "smooth" | "auto";
}

/**
 * https://htmx.org/extensions/#defining
 */
export interface HtmxExtension {
    onEvent?: (name: string, evt: CustomEvent) => any;
    transformResponse?: (text: any, xhr: XMLHttpRequest, elt: any) => any;
    isInlineSwap?: (swapStyle: any) => any;
    handleSwap?: (swapStyle: any, target: any, fragment: any, settleInfo: any) => any;
    encodeParameters?: (xhr: XMLHttpRequest, parameters: any, elt: any) => any;
}


keep keep keep keep replace replace replace keep keep keep keep

Htmx includes a demo WebSockets server written in Node.js that will help you to see WebSockets in action, and begin

bootstrapping your own WebSockets code. It is located in the /test/ws-sse folder of the htmx distribution. Look at

/test/ws-sse/README.md for instructions on running and using the test server.

+++
title = "websockets"
+++

The `WebSockets` extension enables easy, bi-directional communication
with [Web Sockets](https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications)
servers directly from HTML. This replaces the experimental `hx-ws` attribute built into previous versions of htmx. For
help migrating from older versions, see the [Migrating](#migrating-from-previous-versions) guide at the bottom of this page.

Use the following attributes to configure how WebSockets behave:

* `ws-connect="<url>"` or `ws-connect="<prefix>:<url>"` - A URL to establish an `WebSocket` connection against.
* Prefixes `ws` or `wss` can optionally be specified. If not specified, HTMX defaults to add the location's scheme-type,
  host and port to have browsers send cookies via websockets.
* `ws-send` - Sends a message to the nearest websocket based on the trigger value for the element (either the natural
  event
  or the event specified by [`hx-trigger`])

## Install

```html
<script src="https://unpkg.com/htmx.org/dist/ext/ws.js"></script>
```

## Usage

```html

<div hx-ext="ws" ws-connect="/chatroom">
    <div id="notifications"></div>
    <div id="chat_room">
        ...
    </div>
    <form id="form" ws-send>
        <input name="chat_message">
    </form>
</div>
```

### Configuration

WebSockets extension support two configuration options:

- `createWebSocket` - a factory function that can be used to create a custom WebSocket instances. Must be a function,
  returning `WebSocket` object
- `wsBinaryType` - a string value, that defines
  socket's [`binaryType`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket/binaryType) property. Default value
  is `blob`

### Receiving Messages from a WebSocket

The example above establishes a WebSocket to the `/chatroom` end point. Content that is sent down from the websocket
will
be parsed as HTML and swapped in by the `id` property, using the same logic
as [Out of Band Swaps](@/attributes/hx-swap-oob.md).

As such, if you want to change the swapping method (e.g., append content at the end of an element or delegate swapping
to an extension),
you need to specify that in the message body, sent by the server.

```html
<!-- will be interpreted as hx-swap-oob="true" by default -->
<form id="form">
    ...
</form>
<!-- will be appended to #notifications div -->
<div id="notifications" hx-swap-oob="beforeend">
    New message received
</div>
<!-- will be swapped using an extension -->
<div id="chat_room" hx-swap-oob="morphdom">
    ....
</div>
```

### Sending Messages to a WebSocket

In the example above, the form uses the `ws-send` attribute to indicate that when it is submitted, the form values
should be **serialized as JSON**
and send to the nearest enclosing `WebSocket`, in this case the `/chatroom` endpoint.

The serialized values will include a field, `HEADERS`, that includes the headers normally submitted with an htmx
request.

### Automatic Reconnection

If the WebSocket is closed unexpectedly, due to `Abnormal Closure`, `Service Restart` or `Try Again Later`, this
extension will attempt to reconnect until the connection is reestablished.

By default, the extension uses a
full-jitter [exponential-backoff algorithm](https://en.wikipedia.org/wiki/Exponential_backoff) that chooses a randomized
retry delay that grows exponentially over time. You can use a different algorithm by writing it
into `htmx.config.wsReconnectDelay`. This function takes a single parameter, the number of retries, and returns the
time (in milliseconds) to wait before trying again.

```javascript
// example reconnect delay that you shouldn't use because
// it's not as good as the algorithm that's already in place
htmx.config.wsReconnectDelay = function (retryCount) {
    return retryCount * 1000 // return value in milliseconds
}
```

The extension also implements a simple queuing mechanism that keeps messages in memory when the socket is not in `OPEN`
state and sends them once the connection is restored.

### Events

WebSockets extensions exposes a set of events that allow you to observe and customize its behavior.

#### Event - `htmx:wsConnecting` {#htmx:wsConnecting}

This event is triggered when a connection to a WebSocket endpoint is being attempted.

##### Details

* `detail.event.type` - the type of the event (`'connecting'`)

#### Event - `htmx:wsOpen` {#htmx:wsOpen}

This event is triggered when a connection to a WebSocket endpoint has been established.

##### Details

* `detail.elt` - the element that holds the socket (the one with `ws-connect` attribute)
* `detail.event` - the original event from the socket
* `detail.socketWrapper` - the wrapper around socket object

#### Event - `htmx:wsClose` {#htmx:wsClose}

This event is triggered when a connection to a WebSocket endpoint has been closed normally.
You can check if the event was caused by an error by inspecting `detail.event` property.

##### Details

* `detail.elt` - the element that holds the socket (the one with `ws-connect` attribute)
* `detail.event` - the original event from the socket
* `detail.socketWrapper` - the wrapper around socket object

#### Event - `htmx:wsError` {#htmx:wsError}

This event is triggered when `onerror` event on a socket is raised.

##### Details

* `detail.elt` - the element that holds the socket (the one with `ws-connect` attribute)
* `detail.error` - the error object
* `detail.socketWrapper` - the wrapper around socket object

#### Event - `htmx:wsBeforeMessage` {#htmx:wsBeforeMessage}

This event is triggered when a message has just been received by a socket, similar to `htmx:beforeOnLoad`. This event
fires
before any processing occurs.

If the event is cancelled, no further processing will occur.

* `detail.elt` - the element that holds the socket (the one with `ws-connect` attribute)
* `detail.message` - raw message content
* `detail.socketWrapper` - the wrapper around socket object

#### Event - `htmx:wsAfterMessage` {#htmx:wsAfterMessage}

This event is triggered when a message has been completely processed by htmx and all changes have been
settled, similar to `htmx:afterOnLoad`.

Cancelling this event has no effect.

* `detail.elt` - the element that holds the socket (the one with `ws-connect` attribute)
* `detail.message` - raw message content
* `detail.socketWrapper` - the wrapper around socket object

#### Event - `htmx:wsConfigSend` {#htmx:wsConfigSend}

This event is triggered when preparing to send a message from `ws-send` element.
Similarly to [`htmx:configRequest`](@/events.md#htmx:configRequest), it allows you to modify the message
before sending.

If the event is cancelled, no further processing will occur and no messages will be sent.

##### Details

* `detail.parameters` - the parameters that will be submitted in the request
* `detail.unfilteredParameters` - the parameters that were found before filtering
  by [`hx-select`](@/attributes/hx-select.md)
* `detail.headers` - the request headers. Will be attached to the body in `HEADERS` property, if not falsy
* `detail.errors` - validation errors. Will prevent sending and
  trigger [`htmx:validation:halted`](@/events.md#htmx:validation:halted) event if not empty
* `detail.triggeringEvent` - the event that triggered sending
* `detail.messageBody` - raw message body that will be sent to the socket. Undefined, can be set to value of any type,
  supported by WebSockets. If set, will override
  default JSON serialization. Useful, if you want to use some other format, like XML or MessagePack
* `detail.elt` - the element that dispatched the sending (the one with `ws-send` attribute)
* `detail.socketWrapper` - the wrapper around socket object

#### Event - `htmx:wsBeforeSend` {#htmx:wsBeforeSend}

This event is triggered just before sending a message. This includes messages from the queue.
Message can not be modified at this point.

If the event is cancelled, the message will be discarded from the queue and not sent.

##### Details

* `detail.elt` - the element that dispatched the request (the one with `ws-connect` attribute)
* `detail.message` - the raw message content
* `detail.socketWrapper` - the wrapper around socket object

#### Event - `htmx:wsAfterSend` {#htmx:wsAfterSend}

This event is triggered just after sending a message. This includes messages from the queue.

Cancelling the event has no effect.

##### Details

* `detail.elt` - the element that dispatched the request (the one with `ws-connect` attribute)
* `detail.message` - the raw message content
* `detail.socketWrapper` - the wrapper around socket object

#### Socket wrapper

You may notice that all events expose `detail.socketWrapper` property. This wrapper holds the socket
object itself and the message queue. It also encapsulates reconnection algorithm. It exposes a few members:

- `send(message, fromElt)` - sends a message safely. If the socket is not open, the message will be persisted in the
  queue
  instead and sent when the socket is ready.
- `sendImmediately(message, fromElt)` - attempts to send a message regardless of socket state, bypassing the queue. May
  fail
- `queue` - an array of messages, awaiting in the queue.

This wrapper can be used in your event handlers to monitor and manipulate the queue (e.g., you can reset the queue when
reconnecting), and to send additional messages (e.g., if you want to send data in batches).
The `fromElt` parameter is optional and, when specified, will trigger corresponding websocket events from
specified element, namely `htmx:wsBeforeSend` and `htmx:wsAfterSend` events when sending your messages.

### Testing with the Demo Server

Htmx includes a demo WebSockets server written in Go that will help you to see WebSockets in action, and begin
bootstrapping your own WebSockets code. It is located in the /test/servers/ws folder of the htmx distribution. Look at
/test/servers/ws/README.md for instructions on running and using the test server.

### Migrating from Previous Versions

Previous versions of htmx used a built-in tag `hx-ws` to implement WebSockets. This code has been migrated into an
extension instead. Here are the steps you need to take to migrate to this version:

| Old Attribute           | New Attribute        | Comments                                                                                |
|-------------------------|----------------------|----------------------------------------------------------------------------------------------------------------------------------|
| `hx-ws=""`              | `hx-ext="ws"`        | Use the `hx-ext="ws"` attribute to install the WebSockets extension into any HTML element.                                       |
| `hx-ws="connect:<url>"` | `ws-connect="<url>"` | Add a new attribute `ws-connect` to the tag that defines the extension to specify the URL of the WebSockets server you're using. |
| `hx-ws="send"`          | `ws-send=""`         | Add a new attribute `ws-send` to mark any child forms that should send data to your WebSocket server                             |


    <meta charset="utf-8" />
    <title>Mocha Tests</title>
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="stylesheet" href="../../node_modules/mocha/mocha.css" />

Work Area


describe("hx-get attribute", function() {
    beforeEach(function () {
        this.server = makeServer();
        clearWorkArea();
    });
    afterEach(function () {
        this.server.restore();
        clearWorkArea();
    });

    it('issues a GET request on click and swaps content', function () {
        this.server.respondWith("GET", "/test", "Clicked!");

        var btn = make('<button hx-get="/test">Click Me!</button>')
        btn.click();
        this.server.respond();
        btn.innerHTML.should.equal("Clicked!");
    });

    it('GET does not include surrounding data by default', function () {
        this.server.respondWith("GET", "/test", function (xhr) {
            should.equal(getParameters(xhr)["i1"], undefined);
            xhr.respond(200, {}, "Clicked!");
        });
        make('<form><input name="i1" value="value"/><button id="b1" hx-get="/test">Click Me!</button></form>')
        var btn = byId("b1");
        btn.click();
        this.server.respond();
        btn.innerHTML.should.equal("Clicked!");
    });

    it('GET on form includes its own data by default', function () {
        this.server.respondWith("GET", /\/test.*/, function (xhr) {
            getParameters(xhr)["i1"].should.equal("value");
            xhr.respond(200, {}, "Clicked!");
        });
        var form = make('<form hx-trigger="click" hx-get="/test"><input name="i1" value="value"/><button id="b1">Click Me!</button></form>');
        form.click();
        this.server.respond();
        form.innerHTML.should.equal("Clicked!");
    });

    it('GET on form with existing parameters works properly', function () {
        this.server.respondWith("GET", /\/test.*/, function (xhr) {
            getParameters(xhr)["foo"].should.equal("bar");
            getParameters(xhr)["i1"].should.equal("value");
            xhr.respond(200, {}, "Clicked!");
        });
        var form = make('<form hx-trigger="click" hx-get="/test?foo=bar"><input name="i1" value="value"/><button id="b1">Click Me!</button></form>');
        form.click();
        this.server.respond();
        form.innerHTML.should.equal("Clicked!");
    });

    it('GET on form with anchor works properly', function () {
        this.server.respondWith("GET", /\/test.*/, function (xhr) {
            getParameters(xhr)["foo"].should.equal("bar");
            getParameters(xhr)["i1"].should.equal("value");
            xhr.respond(200, {}, "Clicked!");
        });
        var form = make('<form hx-trigger="click" hx-get="/test?foo=bar#foo"><input name="i1" value="value"/><button id="b1">Click Me!</button></form>');
        form.click();
        this.server.respond();
        form.innerHTML.should.equal("Clicked!");
    });

    it('issues a GET request on click and swaps content w/ data-* prefix', function () {
        this.server.respondWith("GET", "/test", "Clicked!");

        var btn = make('<button data-hx-get="/test">Click Me!</button>')
        btn.click();
        this.server.respond();
        btn.innerHTML.should.equal("Clicked!");
    });

    it('does not include a cache-busting parameter when not enabled', function () {

        this.server.respondWith("GET", /\/test.*/, function (xhr) {
            should.not.exist(getParameters(xhr)["org.htmx.cache-buster"]);
            xhr.respond(200, {}, "Clicked!");
        });

        try {
            htmx.config.getCacheBusterParam = false;
            var btn = make('<button hx-get="/test">Click Me!</button>')
            btn.click();
            this.server.respond();
            btn.innerHTML.should.equal("Clicked!");
        } finally {
            htmx.config.getCacheBusterParam = false;
        }
    });

    it('includes a cache-busting parameter when enabled w/ value "true" if no id on target', function () {

        this.server.respondWith("GET", /\/test.*/, function (xhr) {
            getParameters(xhr)["org.htmx.cache-buster"].should.equal("true");
            xhr.respond(200, {}, "Clicked!");
        });

        try {
            htmx.config.getCacheBusterParam = true;
            var btn = make('<button hx-get="/test">Click Me!</button>')
            btn.click();
            this.server.respond();
            btn.innerHTML.should.equal("Clicked!");
        } finally {
            htmx.config.getCacheBusterParam = false;
        }
    });

    it('includes a cache-busting parameter when enabled w/ the id of the target if there is one', function () {
        this.server.respondWith("GET", /\/test.*/, function (xhr) {
            getParameters(xhr)["org.htmx.cache-buster"].should.equal("foo");
            xhr.respond(200, {}, "Clicked!");
        });

        try {
            htmx.config.getCacheBusterParam = true;
            var btn = make('<button hx-get="/test" id="foo">Click Me!</button>')
            btn.click();
            this.server.respond();
            btn.innerHTML.should.equal("Clicked!");
        } finally {
            htmx.config.getCacheBusterParam = false;
        }
    });
});


+++
title = "Keyboard Shortcuts"
template = "demo.html"
+++

In this example we show how to create a keyboard shortcut for an action.

We start with a simple button that loads some content from the server:

```html
<button hx-trigger="click, keyup[altKey&&shiftKey&&key=='D'] from:body"
        hx-post="/doit">Do It! (alt-shift-D)</button>
```

Note that the button responds to both the `click` event (as usual) and also the keyup event when `alt-shift-D` is pressed.
The `from:` modifier is used to listen for the keyup event on the `body` element, thus making it a "global" keyboard
shortcut.

You can trigger the demo below by either clicking on the button, or by hitting alt-shift-D.

You can find out the conditions needed for a given keyboard shortcut here:

https://javascript.info/keyboard-events

{{ demoenv() }}

<script>

    //=========================================================================
    // Fake Server Side Code
    //=========================================================================

    // routes
    init("/init", function(request, params){
        return "<button style='font-size:20pt' hx-trigger='click, keyup[altKey&&shiftKey&&key==\"D\"] from:body'" +
                      " hx-post='/doit'>Do It! (alt-shift-D) </button>";
    });

    onPost("/doit", function (request, params) {
        return "Did it!";
    });

</script>


keep keep keep replace keep keep keep keep keep

/* eslint-env qunit, browser */
let player;
let tech;

import Html5 from '../../../src/js/tech/html5.js';
import * as browser from '../../../src/js/utils/browser.js';
import document from 'global/document';
import sinon from 'sinon';

QUnit.module('HTML5', {
  beforeEach(assert) {
    const el = document.createElement('div');

    el.innerHTML = '<div />';
    player = {
      id() {
        return 'id';
      },
      el() {
        return el;
      },
      options_: {},
      options() {
        return this.options_;
      },
      bufferedPercent() {
        return 0;
      },
      controls() {
        return false;
      },
      usingNativeControls() {
        return false;
      },
      on() {
        return this;
      },
      off() {
        return this;
      },
      ready() {},
      addChild() {},
      trigger() {}
    };
    tech = new Html5({});
  },
  afterEach(assert) {
    tech.dispose();
    player = null;
    tech = null;
  }
});

QUnit[browser.IS_ANY_SAFARI ? 'test' : 'skip']('if setScrubbing is true and fastSeek is available, use it', function(assert) {
  Object.defineProperty(tech.el(), 'currentTime', {
    get: () => {},
    set: () => {},

    writeable: true,
    enumerable: false,
    configurable: true
  });

  const currentTimeSpy = sinon.spy(tech.el(), 'currentTime', ['set']);

  tech.setCurrentTime(5);
  assert.ok(currentTimeSpy.set.called, 'currentTime setter was called');
  assert.ok(currentTimeSpy.set.calledWith(5), 'currentTime setter was called with 5');

  tech.setScrubbing(true);

  // when scrubbing is set but fastSeek isn't available, currentTime should still be called
  tech.el().fastSeek = null;
  tech.setCurrentTime(10);
  assert.ok(currentTimeSpy.set.called, 'currentTime setter was called');
  assert.ok(currentTimeSpy.set.calledWith(10), 'currentTime setter was called with 10');

  const fastSeekSpy = tech.el().fastSeek = sinon.spy();

  tech.setCurrentTime(15);
  assert.ok(currentTimeSpy.set.calledTwice, 'currentTime setter was only called twice and not a 3rd time for fastSeek');
  assert.ok(fastSeekSpy.called, 'fastSeek called');
  assert.ok(fastSeekSpy.calledWith(15), 'fastSeek called with 15');
});

QUnit.test('should be able to set playsinline attribute', function(assert) {
  assert.expect(2);

  tech.createEl();
  tech.setPlaysinline(true);

  assert.ok(tech.el().hasAttribute('playsinline'), 'playsinline attribute was added');

  tech.setPlaysinline(false);

  assert.ok(!tech.el().hasAttribute('playsinline'), 'playsinline attribute was removed');
});

QUnit.test('should detect whether the volume can be changed', function(assert) {

  if (!{}.__defineSetter__) {
    assert.ok(true, 'your browser does not support this test, skipping it');
    return;
  }
  const testVid = Html5.TEST_VID;
  const ConstVolumeVideo = function() {
    this.volume = 1;
    this.__defineSetter__('volume', function() {});
  };

  Html5.TEST_VID = new ConstVolumeVideo();

  assert.ok(!Html5.canControlVolume());
  Html5.TEST_VID = testVid;
});

QUnit.test('test playbackRate', function(assert) {
  // Android 2.3 always returns 0 for playback rate
  if (!Html5.canControlPlaybackRate()) {
    assert.ok(true, 'Playback rate is not supported');
    return;
  }

  tech.createEl();

  tech.el().playbackRate = 1.25;
  assert.strictEqual(tech.playbackRate(), 1.25, 'can be changed from the element');

  tech.setPlaybackRate(0.75);
  assert.strictEqual(tech.playbackRate(), 0.75, 'can be changed from the API');
});

QUnit.test('test defaultPlaybackRate', function(assert) {
  // Android 2.3 always returns 0 for playback rate
  if (!Html5.canControlPlaybackRate()) {
    assert.ok(true, 'Playback rate is not supported');
    return;
  }

  tech.createEl();

  tech.el().defaultPlaybackRate = 1.25;
  assert.strictEqual(tech.defaultPlaybackRate(), 1.25, 'can be changed from the element');

  tech.setDefaultPlaybackRate(0.75);
  assert.strictEqual(tech.defaultPlaybackRate(), 0.75, 'can be changed from the API');
});

QUnit.test('blacklist playbackRate support on older verisons of Chrome on Android', function(assert) {
  if (!Html5.canControlPlaybackRate()) {
    assert.ok(true, 'playbackRate is not supported');
    return;
  }

  // Reset playbackrate - Firefox's rounding of playbackRate causes the rate not to change in canControlPlaybackRate() after a few instances
  Html5.TEST_VID.playbackRate = 1;

  const oldIsAndroid = browser.IS_ANDROID;
  const oldIsChrome = browser.IS_CHROME;
  const oldChromeVersion = browser.CHROME_VERSION;

  browser.stub_IS_ANDROID(true);
  browser.stub_IS_CHROME(true);
  browser.stub_CHROME_VERSION(50);
  assert.strictEqual(Html5.canControlPlaybackRate(), false, 'canControlPlaybackRate should return false on older Chrome');

  browser.stub_CHROME_VERSION(58);
  assert.strictEqual(Html5.canControlPlaybackRate(), true, 'canControlPlaybackRate should return true on newer Chrome');

  browser.stub_IS_ANDROID(oldIsAndroid);
  browser.stub_IS_CHROME(oldIsChrome);
  browser.stub_CHROME_VERSION(oldChromeVersion);
});

QUnit.test('test volume', function(assert) {
  if (!Html5.canControlVolume()) {
    assert.ok(true, 'Volume is not supported');
    return;
  }

  tech.createEl();

  tech.el().volume = 0.5;
  assert.strictEqual(tech.volume(), 0.5, 'can be changed from the element');

  tech.setVolume(1);
  assert.strictEqual(tech.volume(), 1, 'can be changed from the API');
});

QUnit.test('test defaultMuted', function(assert) {
  tech.createEl();

  tech.el().defaultMuted = true;
  assert.strictEqual(tech.defaultMuted(), true, 'can be changed from the element');

  tech.setDefaultMuted(false);
  assert.strictEqual(tech.defaultMuted(), false, 'can be changed from the API');
});

QUnit.test('should export played', function(assert) {
  tech.createEl();
  assert.deepEqual(tech.played(), tech.el().played, 'returns the played attribute');
});

QUnit.test('should remove the controls attribute when recreating the element', function(assert) {
  player.tagAttributes = {
    controls: true
  };
  // force custom controls so the test environment is equivalent on iOS
  player.options_.nativeControlsForTouch = false;
  const el = tech.createEl();

  // On the iPhone controls are always true
  if (!browser.IS_IPHONE) {
    assert.ok(!el.controls, 'controls attribute is absent');
  }

  assert.ok(player.tagAttributes.controls, 'tag attribute is still present');
});

QUnit.test('patchCanPlayType patches canplaytype with our function, conditionally', function(assert) {
  // the patch runs automatically so we need to first unpatch
  Html5.unpatchCanPlayType();

  const oldAV = browser.ANDROID_VERSION;
  const oldIsFirefox = browser.IS_FIREFOX;
  const oldIsChrome = browser.IS_CHROME;
  const video = document.createElement('video');
  const canPlayType = Html5.TEST_VID.constructor.prototype.canPlayType;

  browser.stub_ANDROID_VERSION(4.0);
  browser.stub_IS_FIREFOX(false);
  browser.stub_IS_CHROME(false);
  Html5.patchCanPlayType();

  assert.notStrictEqual(
    video.canPlayType,
    canPlayType,
    'original canPlayType and patched canPlayType should not be equal'
  );

  const patchedCanPlayType = video.canPlayType;
  const unpatchedCanPlayType = Html5.unpatchCanPlayType();

  assert.strictEqual(
    canPlayType,
    Html5.TEST_VID.constructor.prototype.canPlayType,
    'original canPlayType and unpatched canPlayType should be equal'
  );
  assert.strictEqual(
    patchedCanPlayType,
    unpatchedCanPlayType,
    'patched canPlayType and function returned from unpatch are equal'
  );

  browser.stub_ANDROID_VERSION(oldAV);
  browser.stub_IS_FIREFOX(oldIsFirefox);
  browser.stub_IS_CHROME(oldIsChrome);
  Html5.unpatchCanPlayType();
});

QUnit.test('patchCanPlayType doesn\'t patch canplaytype with our function in Chrome for Android', function(assert) {
  // the patch runs automatically so we need to first unpatch
  Html5.unpatchCanPlayType();

  const oldAV = browser.ANDROID_VERSION;
  const oldIsChrome = browser.IS_CHROME;
  const oldIsFirefox = browser.IS_FIREFOX;
  const video = document.createElement('video');
  const canPlayType = Html5.TEST_VID.constructor.prototype.canPlayType;

  browser.stub_ANDROID_VERSION(4.0);
  browser.stub_IS_CHROME(true);
  browser.stub_IS_FIREFOX(false);
  Html5.patchCanPlayType();

  assert.strictEqual(
    video.canPlayType,
    canPlayType,
    'original canPlayType and patched canPlayType should be equal'
  );

  browser.stub_ANDROID_VERSION(oldAV);
  browser.stub_IS_CHROME(oldIsChrome);
  browser.stub_IS_FIREFOX(oldIsFirefox);
  Html5.unpatchCanPlayType();
});

QUnit.test('patchCanPlayType doesn\'t patch canplaytype with our function in Firefox for Android', function(assert) {
  // the patch runs automatically so we need to first unpatch
  Html5.unpatchCanPlayType();

  const oldAV = browser.ANDROID_VERSION;
  const oldIsFirefox = browser.IS_FIREFOX;
  const oldIsChrome = browser.IS_CHROME;
  const video = document.createElement('video');
  const canPlayType = Html5.TEST_VID.constructor.prototype.canPlayType;

  browser.stub_ANDROID_VERSION(4.0);
  browser.stub_IS_FIREFOX(true);
  browser.stub_IS_CHROME(false);
  Html5.patchCanPlayType();

  assert.strictEqual(
    video.canPlayType,
    canPlayType,
    'original canPlayType and patched canPlayType should be equal'
  );

  browser.stub_ANDROID_VERSION(oldAV);
  browser.stub_IS_FIREFOX(oldIsFirefox);
  browser.stub_IS_CHROME(oldIsChrome);
  Html5.unpatchCanPlayType();
});

QUnit.test('should return maybe for HLS urls on Android 4.0 or above when not Chrome or Firefox', function(assert) {
  const oldAV = browser.ANDROID_VERSION;
  const oldIsFirefox = browser.IS_FIREFOX;
  const oldIsChrome = browser.IS_CHROME;
  const video = document.createElement('video');

  browser.stub_ANDROID_VERSION(4.0);
  browser.stub_IS_FIREFOX(false);
  browser.stub_IS_CHROME(false);
  Html5.patchCanPlayType();

  assert.strictEqual(
    video.canPlayType('application/x-mpegurl'),
    'maybe',
    'android version 4.0 or above should be a maybe for x-mpegurl'
  );
  assert.strictEqual(
    video.canPlayType('application/x-mpegURL'),
    'maybe',
    'android version 4.0 or above should be a maybe for x-mpegURL'
  );
  assert.strictEqual(
    video.canPlayType('application/vnd.apple.mpegurl'),
    'maybe',
    'android version 4.0 or above should be a ' +
                    'maybe for vnd.apple.mpegurl'
  );
  assert.strictEqual(
    video.canPlayType('application/vnd.apple.mpegURL'),
    'maybe',
    'android version 4.0 or above should be a ' +
                    'maybe for vnd.apple.mpegurl'
  );

  browser.stub_ANDROID_VERSION(oldAV);
  browser.stub_IS_FIREFOX(oldIsFirefox);
  browser.stub_IS_CHROME(oldIsChrome);
  Html5.unpatchCanPlayType();
});

QUnit.test('error events may not set the errors property', function(assert) {
  assert.equal(tech.error(), undefined, 'no tech-level error');
  tech.trigger('error');
  assert.ok(true, 'no error was thrown');
});

QUnit.test('should have the source handler interface', function(assert) {
  assert.ok(Html5.registerSourceHandler, 'has the registerSourceHandler function');
});

QUnit.test('native source handler canPlayType', function(assert) {
  // Stub the test video canPlayType (used in canPlayType) to control results
  const origCPT = Html5.TEST_VID.canPlayType;

  Html5.TEST_VID.canPlayType = function(type) {
    if (type === 'video/mp4') {
      return 'maybe';
    }
    return '';
  };

  const canPlayType = Html5.nativeSourceHandler.canPlayType;

  assert.equal(
    canPlayType('video/mp4'),
    'maybe',
    'Native source handler reported type support'
  );
  assert.equal(canPlayType('foo'), '', 'Native source handler handled bad type');

  // Reset test video canPlayType
  Html5.TEST_VID.canPlayType = origCPT;
});

QUnit.test('native source handler canHandleSource', function(assert) {
  // Stub the test video canPlayType (used in canHandleSource) to control results
  const origCPT = Html5.TEST_VID.canPlayType;

  Html5.TEST_VID.canPlayType = function(type) {
    if (type === 'video/mp4') {
      return 'maybe';
    }
    return '';
  };

  const canHandleSource = Html5.nativeSourceHandler.canHandleSource;

  assert.equal(
    canHandleSource({ type: 'video/mp4', src: 'video.flv' }, {}),
    'maybe',
    'Native source handler reported type support'
  );
  assert.equal(
    canHandleSource({ src: 'http://www.example.com/video.mp4' }, {}),
    'maybe',
    'Native source handler reported extension support'
  );
  assert.equal(
    canHandleSource({ src: 'https://example.com/video.sd.mp4?s=foo&token=bar' }, {}),
    'maybe',
    'Native source handler reported extension support'
  );
  assert.equal(
    canHandleSource({ src: 'https://example.com/video.sd.mp4?s=foo' }, {}),
    'maybe',
    'Native source handler reported extension support'
  );

  // Test for issue videojs/video.js#1785 and other potential failures
  assert.equal(
    canHandleSource({ src: '' }, {}),
    '',
    'Native source handler handled empty src'
  );
  assert.equal(
    canHandleSource({}, {}),
    '',
    'Native source handler handled empty object'
  );
  assert.equal(
    canHandleSource({ src: 'foo' }, {}),
    '',
    'Native source handler handled bad src'
  );
  assert.equal(
    canHandleSource({ type: 'foo' }, {}),
    '',
    'Native source handler handled bad type'
  );

  // Reset test video canPlayType
  Html5.TEST_VID.canPlayType = origCPT;
});

if (Html5.supportsNativeTextTracks()) {
  QUnit.test('add native textTrack listeners on startup', function(assert) {
    const adds = [];
    const rems = [];
    const tt = {
      length: 0,
      addEventListener: (type, fn) => adds.push([type, fn]),
      removeEventListener: (type, fn) => rems.push([type, fn])
    };
    const el = document.createElement('div');

    el.textTracks = tt;

    const htmlTech = new Html5({el});

    assert.equal(adds[0][0], 'change', 'change event handler added');
    assert.equal(adds[1][0], 'addtrack', 'addtrack event handler added');
    assert.equal(adds[2][0], 'removetrack', 'removetrack event handler added');

    htmlTech.dispose();
  });

  QUnit.test('does not add native textTrack listeners when disabled', function(assert) {
    const events = [];
    const tt = {
      length: 0,
      addEventListener: (type, fn) => events.push([type, fn]),
      removeEventListener: (type, fn) => events.push([type, fn])
    };
    const el = document.createElement('video');

    Object.defineProperty(el, 'textTracks', {
      get: () => tt
    });

    const htmlTech = new Html5({el, nativeTextTracks: false});

    assert.equal(events.length, 0, 'no listeners added');

    const htmlTechAlternate = new Html5({el, nativeCaptions: false});

    assert.equal(events.length, 0, 'no listeners added');

    htmlTech.dispose();
    htmlTechAlternate.dispose();
  });

  QUnit.test('remove all tracks from emulated list on dispose', function(assert) {
    const adds = [];
    const rems = [];
    const tt = {
      length: 0,
      addEventListener: (type, fn) => adds.push([type, fn]),
      removeEventListener: (type, fn) => rems.push([type, fn])
    };
    const el = document.createElement('div');

    el.textTracks = tt;

    const htmlTech = new Html5({el});

    htmlTech.dispose();

    assert.equal(adds[0][0], 'change', 'change event handler added');
    assert.equal(adds[1][0], 'addtrack', 'addtrack event handler added');
    assert.equal(adds[2][0], 'removetrack', 'removetrack event handler added');
    assert.equal(rems[0][0], 'change', 'change event handler removed');
    assert.equal(rems[1][0], 'addtrack', 'addtrack event handler removed');
    assert.equal(rems[2][0], 'removetrack', 'removetrack event handler removed');
    assert.equal(adds[0][0], rems[0][0], 'change event handler removed');
    assert.equal(adds[1][0], rems[1][0], 'addtrack event handler removed');
    assert.equal(adds[2][0], rems[2][0], 'removetrack event handler removed');
  });
}

if (Html5.supportsNativeAudioTracks()) {
  QUnit.test('add native audioTrack listeners on startup', function(assert) {
    const adds = [];
    const rems = [];
    const at = {
      length: 0,
      addEventListener: (type, fn) => adds.push([type, fn]),
      removeEventListener: (type, fn) => rems.push([type, fn])
    };
    const el = document.createElement('div');

    el.audioTracks = at;

    const htmlTech = new Html5({el});

    assert.equal(adds[0][0], 'change', 'change event handler added');
    assert.equal(adds[1][0], 'addtrack', 'addtrack event handler added');
    assert.equal(adds[2][0], 'removetrack', 'removetrack event handler added');

    htmlTech.dispose();
  });

  QUnit.test('does not add native audioTrack listeners when disabled', function(assert) {
    const events = [];
    const at = {
      length: 0,
      addEventListener: (type, fn) => events.push([type, fn]),
      removeEventListener: (type, fn) => events.push([type, fn])
    };
    const el = document.createElement('div');

    el.audioTracks = at;

    const htmlTech = new Html5({el, nativeAudioTracks: false});

    assert.equal(events.length, 0, 'no listeners added');

    htmlTech.dispose();
  });

  QUnit.test('remove all tracks from emulated list on dispose', function(assert) {
    const adds = [];
    const rems = [];
    const at = {
      length: 0,
      addEventListener: (type, fn) => adds.push([type, fn]),
      removeEventListener: (type, fn) => rems.push([type, fn])
    };
    const el = document.createElement('div');

    el.audioTracks = at;

    const htmlTech = new Html5({el});

    htmlTech.dispose();

    assert.equal(adds[0][0], 'change', 'change event handler added');
    assert.equal(adds[1][0], 'addtrack', 'addtrack event handler added');
    assert.equal(adds[2][0], 'removetrack', 'removetrack event handler added');
    assert.equal(rems[0][0], 'change', 'change event handler removed');
    assert.equal(rems[1][0], 'addtrack', 'addtrack event handler removed');
    assert.equal(rems[2][0], 'removetrack', 'removetrack event handler removed');
    assert.equal(adds[0][0], rems[0][0], 'change event handler removed');
    assert.equal(adds[1][0], rems[1][0], 'addtrack event handler removed');
    assert.equal(adds[2][0], rems[2][0], 'removetrack event handler removed');
  });

  QUnit.test('should use overrideNativeTracks on audio correctly', function(assert) {
    assert.expect(8);

    const adds = [];
    const rems = [];
    const at = {
      length: 0,
      addEventListener: (type, fn) => {
        adds.push({ type, fn });
      },
      removeEventListener: (type, fn) => {
        rems.push({ type, fn });
      }
    };

    const el = document.createElement('div');

    el.audioTracks = at;

    const htmlTech = new Html5({el});

    assert.equal(
      adds.length, 3,
      'should have added change, remove, add listeners'
    );
    assert.equal(
      rems.length, 0,
      'no listeners should be removed'
    );

    htmlTech.overrideNativeAudioTracks(true);

    assert.equal(
      adds.length, 3,
      'should not have added additional listeners'
    );
    assert.equal(
      rems.length, 3,
      'should have removed previous three listeners'
    );

    htmlTech.overrideNativeAudioTracks(true);

    assert.equal(
      adds.length, 3,
      'no state change so do not add listeners'
    );
    assert.equal(
      rems.length, 3,
      'no state change so do not remove listeners'
    );

    htmlTech.overrideNativeAudioTracks(false);

    assert.equal(
      adds.length, 6,
      'should add listeners because native tracks should be proxied'
    );
    assert.equal(
      rems.length, 3,
      'should not remove listeners because there where none added on previous state'
    );

    htmlTech.dispose();
  });
}

if (Html5.supportsNativeVideoTracks()) {
  QUnit.test('add native videoTrack listeners on startup', function(assert) {
    const adds = [];
    const rems = [];
    const vt = {
      length: 0,
      addEventListener: (type, fn) => adds.push([type, fn]),
      removeEventListener: (type, fn) => rems.push([type, fn])
    };
    const el = document.createElement('div');

    el.videoTracks = vt;

    const htmlTech = new Html5({el});

    assert.equal(adds[0][0], 'change', 'change event handler added');
    assert.equal(adds[1][0], 'addtrack', 'addtrack event handler added');
    assert.equal(adds[2][0], 'removetrack', 'removetrack event handler added');

    htmlTech.dispose();
  });

  QUnit.test('does not add native audioTrack listeners when disabled', function(assert) {
    const events = [];
    const vt = {
      length: 0,
      addEventListener: (type, fn) => events.push([type, fn]),
      removeEventListener: (type, fn) => events.push([type, fn])
    };
    const el = document.createElement('div');

    el.videoTracks = vt;

    const htmlTech = new Html5({el, nativeVideoTracks: false});

    assert.equal(events.length, 0, 'no listeners added');

    htmlTech.dispose();
  });

  QUnit.test('remove all tracks from emulated list on dispose', function(assert) {
    const adds = [];
    const rems = [];
    const vt = {
      length: 0,
      addEventListener: (type, fn) => adds.push([type, fn]),
      removeEventListener: (type, fn) => rems.push([type, fn])
    };
    const el = document.createElement('div');

    el.videoTracks = vt;

    const htmlTech = new Html5({el});

    htmlTech.dispose();

    assert.equal(adds[0][0], 'change', 'change event handler added');
    assert.equal(adds[1][0], 'addtrack', 'addtrack event handler added');
    assert.equal(adds[2][0], 'removetrack', 'removetrack event handler added');
    assert.equal(rems[0][0], 'change', 'change event handler removed');
    assert.equal(rems[1][0], 'addtrack', 'addtrack event handler removed');
    assert.equal(rems[2][0], 'removetrack', 'removetrack event handler removed');
    assert.equal(adds[0][0], rems[0][0], 'change event handler removed');
    assert.equal(adds[1][0], rems[1][0], 'addtrack event handler removed');
    assert.equal(adds[2][0], rems[2][0], 'removetrack event handler removed');
  });

  QUnit.test('should use overrideNativeTracks on video correctly', function(assert) {
    assert.expect(8);

    const adds = [];
    const rems = [];
    const vt = {
      length: 0,
      addEventListener: (type, fn) => {
        adds.push({ type, fn });
      },
      removeEventListener: (type, fn) => {
        rems.push({ type, fn });
      }
    };
    const el = document.createElement('div');

    el.videoTracks = vt;

    const htmlTech = new Html5({el});

    assert.equal(
      adds.length, 3,
      'should have added change, remove, add listeners'
    );
    assert.equal(
      rems.length, 0,
      'no listeners should be removed'
    );

    htmlTech.overrideNativeVideoTracks(true);

    assert.equal(
      adds.length, 3,
      'should not have added additional listeners'
    );
    assert.equal(
      rems.length, 3,
      'should have removed previous three listeners'
    );

    htmlTech.overrideNativeVideoTracks(true);

    assert.equal(
      adds.length, 3,
      'no state change so do not add listeners'
    );
    assert.equal(
      rems.length, 3,
      'no state change so do not remove listeners'
    );

    htmlTech.overrideNativeVideoTracks(false);

    assert.equal(
      adds.length, 6,
      'should add listeners because native tracks should be proxied'
    );
    assert.equal(
      rems.length, 3,
      'should not remove listeners because there where none added on previous state'
    );

    htmlTech.dispose();
  });
}

QUnit.test('should always return currentSource_ if set', function(assert) {
  const currentSrc = Html5.prototype.currentSrc;

  assert.equal(
    currentSrc.call({el_: {currentSrc: 'test1'}}),
    'test1',
    'sould return source from element if nothing else set'
  );
  assert.equal(
    currentSrc.call({currentSource_: {src: 'test2'}}),
    'test2',
    'sould return source from currentSource_, if nothing else set'
  );
  assert.equal(
    currentSrc.call({currentSource_: {src: 'test2'},
      el_: {currentSrc: 'test1'}}),
    'test2',
    'sould return source from  source set, not from element'
  );
});

QUnit.test('should fire makeup events when a video tag is initialized late', function(assert) {
  const lateInit = Html5.prototype.handleLateInit_;
  let triggeredEvents = [];
  const mockHtml5 = {
    readyListeners: [],
    ready(listener) {
      this.readyListeners.push(listener);
    },
    triggerReady() {
      this.readyListeners.forEach(function(listener) {
        listener.call(this);
      }, this);
    },
    trigger(type) {
      triggeredEvents.push(type);
    },
    on() {},
    off() {}
  };

  function testStates(statesObject, expectedEvents) {
    lateInit.call(mockHtml5, statesObject);
    mockHtml5.triggerReady();
    assert.deepEqual(
      triggeredEvents,
      expectedEvents,
      'wrong events triggered for ' +
                    `networkState:${statesObject.networkState} ` +
                    `and readyState:${statesObject.readyState || 'no readyState'}`
    );

    // reset mock
    triggeredEvents = [];
    mockHtml5.readyListeners = [];
  }

  // Network States
  testStates({ networkState: 0, readyState: 0 }, []);
  testStates({ networkState: 1, readyState: 0 }, ['loadstart']);
  testStates({ networkState: 2, readyState: 0 }, ['loadstart']);
  testStates({ networkState: 3, readyState: 0 }, []);

  // Ready States
  testStates({ networkState: 1, readyState: 0 }, ['loadstart']);
  testStates({ networkState: 1, readyState: 1 }, ['loadstart', 'loadedmetadata']);
  testStates(
    { networkState: 1, readyState: 2 },
    ['loadstart', 'loadedmetadata', 'loadeddata']
  );
  testStates(
    { networkState: 1, readyState: 3 },
    ['loadstart', 'loadedmetadata', 'loadeddata', 'canplay']
  );
  testStates(
    { networkState: 1, readyState: 4 },
    ['loadstart', 'loadedmetadata', 'loadeddata', 'canplay', 'canplaythrough']
  );
});

QUnit.test('Html5.resetMediaElement should remove sources and call load', function(assert) {
  let selector;
  const removedChildren = [];
  let removedAttribute;
  let loaded;
  const children = ['source1', 'source2', 'source3'];
  const testEl = {
    querySelectorAll(input) {
      selector = input;
      return children;
    },

    removeChild(child) {
      removedChildren.push(child);
    },

    removeAttribute(attr) {
      removedAttribute = attr;
    },

    load() {
      loaded = true;
    }
  };

  Html5.resetMediaElement(testEl);
  assert.equal(selector, 'source', 'we got the source elements from the test el');
  assert.deepEqual(
    removedChildren,
    children.reverse(),
    'we removed the children that were present'
  );
  assert.equal(removedAttribute, 'src', 'we removed the src attribute');
  assert.ok(loaded, 'we called load on the element');
});

QUnit.test('Html5#reset calls Html5.resetMediaElement when called', function(assert) {
  const oldResetMedia = Html5.resetMediaElement;
  let resetEl;

  Html5.resetMediaElement = (el) => {
    resetEl = el;
  };

  const el = {};

  Html5.prototype.reset.call({el_: el});

  assert.equal(resetEl, el, 'we called resetMediaElement with the tech\'s el');

  Html5.resetMediaElement = oldResetMedia;
});

QUnit.test('When Android Chrome reports Infinity duration with currentTime 0, return NaN', function(assert) {
  const oldIsAndroid = browser.IS_ANDROID;
  const oldIsChrome = browser.IS_CHROME;
  const oldEl = tech.el_;

  browser.stub_IS_ANDROID(true);
  browser.stub_IS_CHROME(true);

  tech.el_ = {
    duration: Infinity,
    currentTime: 0
  };
  assert.ok(Number.isNaN(tech.duration()), 'returned NaN with currentTime 0');

  browser.stub_IS_ANDROID(oldIsAndroid);
  browser.stub_IS_CHROME(oldIsChrome);
  tech.el_ = oldEl;
});

QUnit.test('supports getting available media playback quality metrics', function(assert) {
  const origPerformance = window.performance;
  const origDate = window.Date;
  const oldEl = tech.el_;
  const videoPlaybackQuality = {
    creationTime: 1,
    corruptedVideoFrames: 2,
    droppedVideoFrames: 3,
    totalVideoFrames: 5
  };

  tech.el_ = {
    getVideoPlaybackQuality: () => videoPlaybackQuality
  };
  assert.deepEqual(
    tech.getVideoPlaybackQuality(),
    videoPlaybackQuality,
    'uses native implementation when supported'
  );

  tech.el_ = {
    webkitDroppedFrameCount: 1,
    webkitDecodedFrameCount: 2
  };
  window.performance = {
    now: () => 4
  };
  assert.deepEqual(
    tech.getVideoPlaybackQuality(),
    { droppedVideoFrames: 1, totalVideoFrames: 2, creationTime: 4 },
    'uses webkit prefixed metrics and performance.now when supported'
  );

  tech.el_ = {
    webkitDroppedFrameCount: 1,
    webkitDecodedFrameCount: 2
  };
  window.Date = {
    now: () => 10
  };
  window.performance = {
    timing: {
      navigationStart: 3
    }
  };
  assert.deepEqual(
    tech.getVideoPlaybackQuality(),
    { droppedVideoFrames: 1, totalVideoFrames: 2, creationTime: 7 },
    'uses webkit prefixed metrics and Date.now() - navigationStart when ' +
                   'supported'
  );

  tech.el_ = {};
  window.performance = void 0;
  assert.deepEqual(tech.getVideoPlaybackQuality(), {}, 'empty object when not supported');

  window.performance = {
    now: () => 5
  };
  assert.deepEqual(
    tech.getVideoPlaybackQuality(),
    { creationTime: 5 },
    'only creation time when it\'s the only piece available'
  );

  window.performance = {
    timing: {
      navigationStart: 3
    }
  };
  assert.deepEqual(
    tech.getVideoPlaybackQuality(),
    { creationTime: 7 },
    'only creation time when it\'s the only piece available'
  );

  tech.el_ = {
    getVideoPlaybackQuality: () => videoPlaybackQuality,
    webkitDroppedFrameCount: 1,
    webkitDecodedFrameCount: 2
  };
  assert.deepEqual(
    tech.getVideoPlaybackQuality(),
    videoPlaybackQuality,
    'prefers native implementation when supported'
  );

  tech.el_ = oldEl;
  window.performance = origPerformance;
  window.Date = origDate;
});

QUnit.test('featuresVideoFrameCallback is false for audio elements', function(assert) {
  const el = document.createElement('audio');
  const audioTech = new Html5({el});

  assert.strictEqual(audioTech.featuresVideoFrameCallback, false, 'Html5 with audio element should not support rvf');

  audioTech.dispose();
});