Back to Repositories

Testing Editor URL Generation Implementation in BetterErrors

This test suite validates the BetterErrors::Editor component’s functionality for handling editor URL formatting and environment configurations. It ensures proper URL generation for different code editors and validates path transformations based on environment variables.

Test Coverage Overview

The test suite provides comprehensive coverage of the Editor class functionality, focusing on URL generation and editor configuration handling.

  • Tests URL formatting for various editor commands and symbols
  • Validates environment variable handling for editor preferences
  • Verifies path transformation logic with virtual and host paths
  • Covers default editor fallback behavior

Implementation Analysis

The testing approach utilizes RSpec’s describe/context/it blocks to organize test cases hierarchically. It employs extensive context-based testing to verify behavior under different environment configurations and editor settings.

  • Uses RSpec’s subject and let declarations for clean test setup
  • Implements before blocks for environment variable configuration
  • Employs shared examples for common editor URL scheme testing

Technical Details

  • Testing Framework: RSpec
  • Environment Configuration: Uses ENV variable mocking
  • Test Dependencies: spec_helper
  • Testing Patterns: Subject/Let declarations, Before hooks, Context blocks
  • Assertion Methods: expect().to, start_with, match

Best Practices Demonstrated

The test suite exemplifies several testing best practices for Ruby and RSpec.

  • Proper test isolation using environment variable cleanup
  • Descriptive context and test descriptions
  • DRY implementation through shared test patterns
  • Comprehensive edge case coverage
  • Clear test organization and structure

bettererrors/better_errors

spec/better_errors/editor_spec.rb

            
require "spec_helper"

RSpec.describe BetterErrors::Editor do
  describe ".for_formatting_string" do
    it "returns an object that reponds to #url" do
      editor = described_class.for_formatting_string("custom://%{file}:%{file_unencoded}:%{line}")
      expect(editor.url("/path&file", 42)).to eq("custom://%2Fpath%26file:/path&file:42")
    end
  end

  describe ".for_proc" do
    it "returns an object that responds to #url, which calls the proc" do
      editor = described_class.for_proc(proc { |file, line| "result" } )
      expect(editor.url("foo", 42)).to eq("result")
    end
  end

  describe ".default_editor" do
    subject(:default_editor) { described_class.default_editor }
    before do
      ENV['BETTER_ERRORS_EDITOR_URL'] = nil
      ENV['BETTER_ERRORS_EDITOR'] = nil
      ENV['EDITOR'] = nil
    end

    it "returns an object that responds to #url" do
        expect(default_editor.url("foo", 123)).to match(/foo/)
    end

    context "when $BETTER_ERRORS_EDITOR_URL is set" do
      before do
        ENV['BETTER_ERRORS_EDITOR_URL'] = "custom://%{file}:%{file_unencoded}:%{line}"
      end

      it "uses the value as a formatting string to build the editor URL" do
        expect(default_editor.url("/path&file", 42)).to eq("custom://%2Fpath%26file:/path&file:42")
      end
    end

    context "when $BETTER_ERRORS_EDITOR is set to one of the preset commands" do
      before do
        ENV['BETTER_ERRORS_EDITOR'] = "subl"
      end

      it "returns an object that builds URLs for the corresponding editor" do
        expect(default_editor.url("foo", 123)).to start_with('subl://')
      end
    end

    context "when $EDITOR is set to one of the preset commands" do
      before do
        ENV['EDITOR'] = "subl"
      end

      it "returns an object that builds URLs for the corresponding editor" do
        expect(default_editor.url("foo", 123)).to start_with('subl://')
      end

      context "when $BETTER_ERRORS_EDITOR is set to one of the preset commands" do
        before do
          ENV['BETTER_ERRORS_EDITOR'] = "emacs"
        end

        it "returns an object that builds URLs for that editor instead" do
          expect(default_editor.url("foo", 123)).to start_with('emacs://')
        end
      end

      context "when $BETTER_ERRORS_EDITOR is set to an unrecognized command" do
        before do
          ENV['BETTER_ERRORS_EDITOR'] = "fubarcmd"
        end

        it "returns an object that builds URLs for the $EDITOR instead" do
          expect(default_editor.url("foo", 123)).to start_with('subl://')
        end
      end
    end

    context "when $EDITOR is set to an unrecognized command" do
      before do
        ENV['EDITOR'] = "fubarcmd"
      end

      it "returns an object that builds URLs for TextMate" do
        expect(default_editor.url("foo", 123)).to start_with('txmt://')
      end
    end

    context "when $EDITOR and $BETTER_ERRORS_EDITOR are not set" do
      it "returns an object that builds URLs for TextMate" do
        expect(default_editor.url("foo", 123)).to start_with('txmt://')
      end
    end
  end

  describe ".editor_from_command" do
    subject { described_class.editor_from_command(command_line) }

    ["atom -w", "/usr/bin/atom -w"].each do |command|
      context "when editor command is '#{command}'" do
        let(:command_line) { command }

        it "uses atom:// scheme" do
          expect(subject.url("file", 42)).to start_with("atom://")
        end
      end
    end

    ["emacsclient", "/usr/local/bin/emacsclient"].each do |command|
      context "when editor command is '#{command}'" do
        let(:command_line) { command }

        it "uses emacs:// scheme" do
          expect(subject.url("file", 42)).to start_with("emacs://")
        end
      end
    end

    ["idea"].each do |command|
      context "when editor command is '#{command}'" do
        let(:command_line) { command }

        it "uses idea:// scheme" do
          expect(subject.url("file", 42)).to start_with("idea://")
        end
      end
    end

    ["mate -w", "/usr/bin/mate -w"].each do |command|
      context "when editor command is '#{command}'" do
        let(:command_line) { command }

        it "uses txmt:// scheme" do
          expect(subject.url("file", 42)).to start_with("txmt://")
        end
      end
    end

    ["mine"].each do |command|
      context "when editor command is '#{command}'" do
        let(:command_line) { command }

        it "uses x-mine:// scheme" do
          expect(subject.url("file", 42)).to start_with("x-mine://")
        end
      end
    end

    ["mvim -f", "/usr/local/bin/mvim -f"].each do |command|
      context "when editor command is '#{command}'" do
        let(:command_line) { command }

        it "uses mvim:// scheme" do
          expect(subject.url("file", 42)).to start_with("mvim://")
        end
      end
    end

    ["subl -w", "/Applications/Sublime Text 2.app/Contents/SharedSupport/bin/subl"].each do |command|
      context "when editor command is '#{command}'" do
        let(:command_line) { command }

        it "uses subl:// scheme" do
          expect(subject.url("file", 42)).to start_with("subl://")
        end
      end
    end

    ["vscode", "code"].each do |command|
      context "when editor command is '#{command}'" do
        let(:command_line) { command }

        it "uses vscode:// scheme" do
          expect(subject.url("file", 42)).to start_with("vscode://")
        end
      end
    end
  end

  describe ".editor_from_symbol" do
    subject { described_class.editor_from_symbol(symbol) }

    [:atom].each do |symbol|
      context "when symbol is '#{symbol}'" do
        let(:symbol) { symbol }

        it "uses atom:// scheme" do
          expect(subject.url("file", 42)).to start_with("atom://")
        end
      end
    end

    [:emacs, :emacsclient].each do |symbol|
      context "when symbol is '#{symbol}'" do
        let(:symbol) { symbol }
        it "uses emacs:// scheme" do
          expect(subject.url("file", 42)).to start_with("emacs://")
        end
      end
    end

    [:macvim, :mvim].each do |symbol|
      context "when symbol is '#{symbol}'" do
        let(:symbol) { symbol }

        it "uses mvim:// scheme" do
          expect(subject.url("file", 42)).to start_with("mvim://")
        end
      end
    end

    [:sublime, :subl, :st].each do |symbol|
      context "when symbol is '#{symbol}'" do
        let(:symbol) { symbol }

        it "uses subl:// scheme" do
          expect(subject.url("file", 42)).to start_with("subl://")
        end
      end
    end

    [:textmate, :txmt, :tm].each do |symbol|
      context "when symbol is '#{symbol}'" do
        let(:symbol) { symbol }

        it "uses txmt:// scheme" do
          expect(subject.url("file", 42)).to start_with("txmt://")
        end
      end
    end
  end

  describe "#url" do
    subject(:url) { described_instance.url("/full/path/to/lib/file.rb", 42) }
    let(:described_instance) { described_class.for_formatting_string("%{file_unencoded}")}
    before do
      ENV['BETTER_ERRORS_VIRTUAL_PATH'] = virtual_path
      ENV['BETTER_ERRORS_HOST_PATH'] = host_path
    end
    let(:virtual_path) { nil }
    let(:host_path) { nil }

    context "when $BETTER_ERRORS_VIRTUAL_PATH is set" do
      let(:virtual_path) { "/full/path/to" }

      context "when $BETTER_ERRORS_HOST_PATH is not set" do
        let(:host_path) { nil }

        it "removes the VIRTUAL_PATH prefix, making the path relative" do
          expect(url).to eq("lib/file.rb")
        end  
      end

      context "when $BETTER_ERRORS_HOST_PATH is set" do
        let(:host_path) { '/Users/myname/Code' }

        it "replaces the VIRTUAL_PATH prefix with the HOST_PATH" do
          expect(url).to eq("/Users/myname/Code/lib/file.rb")
        end  
      end
    end

    context "when $BETTER_ERRORS_VIRTUAL_PATH is not set" do
      it "does not alter file paths" do
        expect(url).to eq("/full/path/to/lib/file.rb")
      end
    end
  end
end