Source: testing/CustomElementTest.js

import DOMCreator from "./DOMCreator.js"
import assert from "assert"
/**
 * The class that implements a test case.  Typically, an instance of this is created for you and you call `test` on that to write your
 * test case.
 * @memberof testing
 */
class CustomElementTest {
  constructor(html, queryString, assetMetadata) {
    this.html          = html
    this.queryString   = queryString
    this.fetchBehavior = {}
    this.assetMetadata = assetMetadata
  }

  /**
   * Configure a query string to be present when the custom elements are defined and connected.
   */
  andQueryString(queryString) {
    this.queryString = queryString
    return this
  }

  /**
   * Configure behavior when #{link external:fetch} is called, since it's not implemented by JSDom or NodeJS.
   *
   * @param {String} url - the URL that is expected. This should be a relative URL, as that is all that is currently supported. This
   * URL must match exactly to a `fetch` call.
   * @param {Object} behavior - an object describing what you want to happen when `url` is fetched.  This currently supports only a
   * few rudimentary behaviors:
   * * `{then: { ok: { text: "some text" } } }` - This will return an "ok" response whose body is the given text, available only as
   * text.
   * * `{then: { status: XXX, text: "some text" }}` - This will return the given http status, with `ok` as true if it's 2xx or 3xx. If `text` is given, that will be available as text only.
   */
  onFetch(url,behavior) {
    if (!this.fetchBehavior[url]) {
      this.fetchBehavior[url] = {
        numCalls: 0,
        responses: [],
      }
    }
    if (behavior instanceof Array) {
      behavior.forEach( (b) =>  {
        this.fetchBehavior[url].responses.push(b)
      })
    }
    else {
      this.fetchBehavior[url].responses.push(behavior)
    }
    return this
  }

  /** Comment out a test without using code comments */
  xtest() {
    return this
  }


  /** Declare a test to run with the previously-defined HTML, query string, and fetch behavior.
   *
   * @param {String} description - a description of the test.
   * @param {testCodeCallback} testCode - a function containing the code for your test.
   */
  test(description,testCode) {
    it(description, () => {
      const domCreator = new DOMCreator(this.assetMetadata)
      const dom = domCreator.create({
        html: this.html,
        queryString: this.queryString
      })
      const fetchRequests = []

      dom.window.Request = Request
      dom.window.fetch = (request) => {
        const url = new URL(request.url)
        const path = url.pathname + url.search
        const behaviors = this.fetchBehavior[path]
        if (!behaviors) {
          throw `fetch() called with ${path}, which was not configured`
        }
        if (behaviors.numCalls > behaviors.responses.length) {
          throw `fetch() called ${behaviors.numCalls} times, but we only have ${behaviors.response.length} responses configured`
        }
        const behavior = behaviors.responses[behaviors.numCalls]
        behaviors.numCalls++

        let promise = null

        if (behavior.then) {
          if (behavior.then.ok) {
            if (behavior.then.ok.text) {
              const response = {
                ok: true,
                text: () => {
                  return Promise.resolve(behavior.then.ok.text)
                }
              }
              promise = Promise.resolve(response)
            }
            else {
              throw `unknown fetch behavior: expected then.ok.text: ${JSON.stringify(behavior)}`
            }
          }
          else if (behavior.then.status) {
            let ok = false
            if ((behavior.then.status >= 200) && (behavior.then.status < 400) ){
              ok = true
            }
            const response = {
              ok: ok,
              status: behavior.then.status,
                text: () => {
                  return Promise.resolve(behavior.then.text)
                }
            }
            promise = Promise.resolve(response)
          }
          else {
            throw `unknown fetch behavior: expected then.ok or then.status: ${JSON.stringify(behavior)}`
          }
        }
        else {
          throw `unknown fetch behavior: expected then: ${JSON.stringify(behavior)}`
        }
        request.promiseReturned = promise
        fetchRequests.push(request)
        return promise
      }

      const window = dom.window
      const document = window.document
      let returnValue = null
      return new Promise( (resolve, reject) => {
        dom.window.addEventListener("load", () => {
          try {
            const paramsGivenToTest = {
              window,
              document,
              assert,
              fetchRequests,
              waitForSetTimeout: this.#waitForSetTimeout,
              readRequestBodyIntoJSON: this.#readRequestBodyIntoJSON,
              readRequestBodyIntoString: this.#readRequestBodyIntoString,
            }
            returnValue = testCode(paramsGivenToTest)
            if (returnValue) {
              resolve(returnValue)
            }
            else {
              resolve()
            }
          } catch (e) {
            reject(e)
          }
        })
      })
    })
    return this
  }

  /** Used to wait for a few milliseconds before performing further assertions.
   * This can be useful when a custom element has `setTimeout` calls
   */
  #waitForSetTimeout = (ms) => {
    return new Promise( (resolve) => {
      setTimeout(resolve,ms)
    })
  }

  /**
   * Given a fetch request (available via `fetchRequests` passed to your test), turn the body into JSON
   * and return a promise with the JSON.  To use this in a test, you must include your assertions inside
   * the `then` of the returned promise and you *must* return that from your test.
   *
   * @example
   * withHTML(
   *   "<div-makes-a-fetch>"
   * ).onFetch("/foo", { then: { status: 200 }}
   * ).test("a test",({assert,fetchRequests}) => {
   *   assert.equal(fetchRequests.length,1)
   *   return readRequestBodyIntoJSON(fetchRequests[0]).then( (json) => {
   *     assert.equal(json["foo"],"bar")
   *   })
   * })
   */
  #readRequestBodyIntoJSON = (request) => {
    return this.#readRequestBodyIntoString(request).then( (string) => {
      try {
        const json = JSON.parse(string)
        return Promise.resolve(json)
      }
      catch (e) {
        assert(false,`'${string}' could not be parsed as JSON`)
      }
      return Promise.resolve()
    })
  }

  /**
   * Given a fetch request (available via `fetchRequests` passed to your test), turn the body into a string
   * and return a promise with the string.  To use this in a test, you must include your assertions inside
   * the `then` of the returned promise and you *must* return that from your test.
   *
   * @example
   * withHTML(
   *   "<div-makes-a-fetch>"
   * ).onFetch("/foo", { then: { status: 200 }}
   * ).test("a test",({assert,fetchRequests}) => {
   *   assert.equal(fetchRequests.length,1)
   *   return readRequestBodyIntoString(fetchRequests[0]).then( (string) => {
   *     assert.equal(string,"foo")
   *   })
   * })
   */
  #readRequestBodyIntoString = (request) => {
    const reader = request.body.getReader()
    const utf8Decoder = new TextDecoder("utf-8");
    let string = ""
    const parse = ({value,done}) => {
      if (done) {
        return Promise.resolve(string)
      }
      else {
        if (value) {
          string = string + utf8Decoder.decode(value, { stream: true })
        }
        return reader.read().then(parse)
      }
    }
    return reader.read().then(parse)
  }


}
export default CustomElementTest