Index [Almirah.Spec] : orama_search_spec.rb

[Almirah.Spec] : orama_search_spec.rb

1.1 References

# UL DL COV DR
SC-AAER Full-text search on the Index page returns matching specification content SRS-077
SC-AAES Index page search indicates when there are no matches SRS-078

1.2 Source Code

# frozen_string_literal: true

require_relative 'spec_helper'
require 'webrick'
require 'ferrum'
require 'json'

# End-to-end test for the Orama client-side search on the rendered Index page
# (ISSUE-183). The search only works when the build is served over HTTP: the JS
# fetches the JSON DB (blocked under file://) and the search box is revealed only
# when document.URL includes 'http' (see templates/scripts/main.js). So this test
# renders a project, serves build/ with WEBrick, and drives the real index.html
# in headless Chrome via ferrum, hitting the live unpkg-hosted Orama bundle.
RSpec.describe 'Orama search on the Index page', type: :aruba do
  def detect_chrome
    bundled = [
      ENV.fetch('BROWSER_PATH', nil),
      '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',
      '/Applications/Chromium.app/Contents/MacOS/Chromium'
    ].compact.find { |p| File.executable?(p) }
    return bundled if bundled

    names = %w[google-chrome-stable google-chrome chromium chromium-browser chrome]
    names.map { |n| `command -v #{n} 2>/dev/null`.strip }.find { |r| !r.empty? }
  end

  def wait_until(timeout: 45, interval: 0.3)
    deadline = Time.now + timeout
    loop do
      result = yield
      return result if result
      raise "Timed out after #{timeout}s waiting for the browser condition" if Time.now > deadline

      sleep interval
    end
  end

  # Sets the search box value and fires the real keyup handler. Re-runnable so the
  # polling loop tolerates the module's async load (DB not yet indexed on early tries).
  def dispatch_search(term)
    @browser.evaluate(<<~JS)
      (() => {
        const e = document.getElementById('searchInput');
        if (!e) return false;
        e.value = #{term.to_json};
        e.dispatchEvent(new KeyboardEvent('keyup', { bubbles: true }));
        return true;
      })()
    JS
  end

  def dropdown_text
    @browser.evaluate("(document.getElementById('search_dropdown') || {}).textContent || ''")
  end

  before do
    chrome = detect_chrome
    skip 'Chrome/Chromium not found (set BROWSER_PATH to enable this test)' unless chrome

    write_file('myproject/project.yml', <<~YML)
      specifications:
        input: []
    YML
    write_file('myproject/specifications/req/req.md', <<~MD)
      ---
      title: "Searchable Spec"
      ---

      # Requirements

      [REQ-001] The system shall index the unique token Zorblax for retrieval.
    MD
    run_command_and_stop('almirah please myproject')

    build_dir = expand_path('myproject/build')
    @server = WEBrick::HTTPServer.new(
      BindAddress: '127.0.0.1',
      Port: 0,
      DocumentRoot: build_dir,
      Logger: WEBrick::Log.new(File::NULL),
      AccessLog: []
    )
    @port = @server.listeners.first.addr[1]
    @server_thread = Thread.new { @server.start }

    @browser = Ferrum::Browser.new(
      headless: true,
      browser_path: chrome,
      browser_options: { 'no-sandbox' => nil, 'disable-dev-shm-usage' => nil },
      timeout: 60,
      process_timeout: 30
    )
  end

  after do
    @browser&.quit
    @server&.shutdown
    @server_thread&.join
  end

  # <REQ> Full-text search on the Index page returns matching specification content >[SRS-077] </REQ>
  # <REQ> Index page search indicates when there are no matches >[SRS-078] </REQ>
  it 'returns results for a known term and reports no matches otherwise' do
    @browser.go_to("http://127.0.0.1:#{@port}/index.html")

    # The http-only gate in main.js reveals the search box once the page is served.
    wait_until do
      @browser.evaluate(
        "(() => { const e = document.getElementById('searchInput'); " \
        "return !!e && getComputedStyle(e).display !== 'none'; })()"
      )
    end

    # Positive case: re-dispatch until the async module has loaded the DB and a hit renders.
    wait_until do
      dispatch_search('Zorblax')
      @browser.evaluate("!!document.querySelector('#search_dropdown a')")
    end

    item_count = @browser.evaluate("document.querySelectorAll('#search_dropdown .search-item').length")
    href = @browser.evaluate(
      "(() => { const a = document.querySelector('#search_dropdown a'); " \
      'return a ? a.getAttribute("href") : null; })()'
    )
    text = dropdown_text

    aggregate_failures do
      expect(item_count).to be >= 1
      expect(text).to include('Zorblax')          # snippet -> term was indexed and matched
      expect(text).to include('Searchable Spec')  # doc_title field rendered (ISSUE-183 rename)
      expect(href).to be_a(String).and(be_truthy)
      expect(href).not_to be_empty
    end

    # Negative case: a term that is not in the index shows the empty-state message.
    wait_until do
      dispatch_search('Qwxzplkno')
      dropdown_text.include?('There are no matches found')
    end
    expect(dropdown_text).to include('There are no matches found')
  end
end