assets/test/sidebar_section_test.mjs

58e6347eeb72 · 4 KB

// The rule this pins is a client rule, and it is the rule the docs and
// component-library sidebars used to break: what the reader collapsed wins,
// and the hook never records a choice on the reader's behalf. The server sends
// the same answer -- see `OpenAgentsWeb.SidebarStateTest` -- but only the
// browser decides what happens after a LiveView update repaints the seed.
import assert from "node:assert/strict"
import {execFileSync} from "node:child_process"
import {readdirSync, readFileSync} from "node:fs"
import {resolve} from "node:path"
import test from "node:test"
import {fileURLToPath} from "node:url"

const assetsDir = resolve(fileURLToPath(new URL("..", import.meta.url)))
const projectDir = resolve(assetsDir, "..")
const mixEnv = process.env.MIX_ENV ?? "test"

// The hook is colocated in `layouts.ex`, so the file to load is the one the
// compiler extracts. Compile first if this clone has not been built yet.
const hookPath = () => {
  const directory = resolve(
    projectDir,
    `_build/${mixEnv}/phoenix-colocated/openagents/OpenAgentsWeb.Layouts`,
  )

  const found = readdirSync(directory)
    .filter(entry => entry.endsWith(".js"))
    .map(entry => resolve(directory, entry))
    .find(path => readFileSync(path, "utf8").includes("sidebar_sections"))

  assert.ok(found, "no colocated hook in layouts.ex maintains sidebar_sections")
  return found
}

const loadHook = async () => {
  try {
    return (await import(hookPath())).default
  } catch (_missingBuild) {
    execFileSync("mix", ["compile"], {
      cwd: projectDir,
      encoding: "utf8",
      env: {...process.env, MIX_ENV: mixEnv},
      stdio: "pipe",
    })
    return (await import(hookPath())).default
  }
}

// Enough of a document for a hook that reads and writes one cookie.
const stubDocument = jar => {
  globalThis.document = {
    get cookie() {
      return Object.entries(jar)
        .map(([key, value]) => `${key}=${value}`)
        .join("; ")
    },
    set cookie(assignment) {
      const [pair] = assignment.split("; ")
      const separator = pair.indexOf("=")
      jar[pair.slice(0, separator)] = pair.slice(separator + 1)
    },
  }
}

const cookieValue = sections => encodeURIComponent(JSON.stringify(sections))

// A `<details>` with a summary and, optionally, the row marking the page the
// reader is on.
const stubSection = ({id, open, active}) => {
  const listeners = {}

  return {
    id,
    open,
    dataset: {},
    addEventListener: (name, handler) => (listeners[name] = handler),
    removeEventListener: name => delete listeners[name],
    querySelector: selector => {
      if (selector === "summary") return {addEventListener() {}, removeEventListener() {}}
      if (selector === "[aria-current]") return active ? {} : null
      return null
    },
    toggle(next) {
      this.open = next
      listeners.toggle?.()
    },
  }
}

const mount = async element => {
  const hook = await loadHook()
  const instance = Object.create(hook)
  instance.el = element
  instance.mounted()
  return instance
}

test("a collapsed section the reader is reading stays collapsed", async () => {
  const jar = {sidebar_sections: cookieValue({"sidebar-section-getting-started": false})}
  stubDocument(jar)

  // Painted open, as a LiveView update repainting the seed would leave it.
  const element = stubSection({id: "sidebar-section-getting-started", open: true, active: true})
  await mount(element)

  assert.equal(element.open, false)
  assert.deepEqual(JSON.parse(decodeURIComponent(jar.sidebar_sections)), {
    "sidebar-section-getting-started": false,
  })
})

test("the seed governs a section the reader has said nothing about", async () => {
  const jar = {}
  stubDocument(jar)

  const element = stubSection({id: "sidebar-section-issues", open: true, active: true})
  await mount(element)

  assert.equal(element.open, true)
  // Silence stays silence: an unwritten cookie is what lets the seed keep
  // deciding, including the seed that opens the section holding the page.
  assert.deepEqual(jar, {})
})

test("turning a caret records the reader's choice", async () => {
  const jar = {}
  stubDocument(jar)

  const element = stubSection({id: "sidebar-section-reference", open: true, active: false})
  await mount(element)
  element.toggle(false)

  assert.deepEqual(JSON.parse(decodeURIComponent(jar.sidebar_sections)), {
    "sidebar-section-reference": false,
  })
})