DrSensor / nusa

incremental runtime that bring both simplicity and power into webdev (buildless, cross-language, data-driven)
MIT License
4 stars 0 forks source link

Scoped Live Testing #52

Open DrSensor opened 1 year ago

DrSensor commented 1 year ago

Run test suite on every live reload only on scope that is in the viewport. There's 2 approach based on how test suites got removed on the production code:

  1. Either inline or import test suite in the linked module. This approach rely on tree shaking mechanism in JS bundler.
    
    import { current, scope, use } from "nusa/std"

export default class Counter { accessor count increment() { this.count += +current.target.value } }

import { suite, test } from "@testdeck/jasmine" import { expect } from "chai"

if (TEST) @suite class TestCounter { counter = use(Counter) @test count_when_clicked() { scope(this.counter) .getElementByTagName("button")[0] .click()

  expect(this.counter.count).toBe(1);
}

}

or
```js
import { current } from "nusa/std"

export default class Counter {
  accessor count
  increment() {
    this.count += +current.target.value
  }
}

if (__TEST__) await import("./counter.test.js")
import { suite, test } from "@testdeck/jasmine"
import { expect } from "chai"
import { scope, use } from "nusa/std"

import Counter from "./counter.js"

@suite class TestCounter {
  counter = use(Counter)
  @test count_when_clicked() {
    scope(this.counter)
      .getElementByTagName("button")[0]
      .click()

    expect(this.counter.count).toBe(1);
  }
}

export default TestCounter // for <link>-ing (optional)
  1. <link> test module. This rely on site generator to remove specific elements based on specific attribute.

    <render-scope>
    <link href=./counter.js>
    <link href=./counter.test.js>
    
    <template shadowroot=closed>
    <button :: on:click=increment>++</button>
    <span :: text:=count>0</span>
    </template>
    </render-scope>

    then site generator (i.e lume) can remove the testing code like

    if (!DEV) site.process([".html"], ({ document }) => {
    document
    .querySelectorAll('link[href$=".test.js"]')
    .forEach(element => element.remove())
    })

    Food for Thought

    To make it toolless, use BroadcastChannel to communicate between dev, report, and test environment. For example:

    • localhost:3000 - dev environment. No test are running (__TEST__ === false). Trigger/send test signal via BroadcastChannel or WebSocket when <render-scope> intersect with viewport
    • localhost:??? - test environment. It might run in:
    • headless browser via WebSocket (run in different port i.e localhost:5000)
    • or just new tab or popup without spawning new browser instance (communicate via BroadcastChannel, run in same port i.e localhost:3000)
    • localhost:3000/.well-known/test-report or inside #44 - view and control how test being run
DrSensor commented 1 year ago

I've decided to use zora for testing both demo and internal implementation and expose a <button>test</button> to manually run the tests. Since use() and scope() are context specific, it must be used inside callback like @build decorator or others. So I plan to expose a lifecycle module specifically (but not limited) for testing.

import * as lifecycle from "//esm.run/nusa/lifecycle"
import { current, use } from "//esm.run/nusa/std"
import { test } from "zora"
import Counter from "./module.js"

const testbed = lifecycle.render(() => {
  const counter = use(Counter)
  const button = current.root.getElementsByTagName("button")[0]

  test(name, t => {
    button.click()
    t.equal(counter.count, 1);
  })

  lifecycle.clear(testbed)
})

const name = import.meta.url.split("/").at(-2)
<script type=module src=test.js></script>
<script async src=//esm.run/nusa/render-scope></script>

<render-scope>
  <link href=module.js>

  <template shadowrootmode=closed>
    <button :: on:click=increment text:=count/>
  </template>
</render-scope>