Index [Almirah.Spec] : affected_documents_spec.rb

[Almirah.Spec] : affected_documents_spec.rb

1.1 References

# UL DL COV DR
SC-AAAN Recognises the Affected Documents section and parses its three-column table.
SC-AAAO Req-ID column accepts SRS-054
SC-AAAP Render the Req-ID cell on the Decision page as a clickable link. SRS-057
SC-AAAQ Controlled-paragraph table carries a DR column titled "Decision Record" after COV. SRS-058
SC-AAAR Establishes a link from each Affected Documents row to the referenced Item. SRS-055
SC-AAAS For each Controlled Item show the affecting Decision Records as clickable links. SRS-058
SC-AAAT Click on a decision record link navigates to the Decision Record page. SRS-060
SC-AAAU Multi-link DR cell uses the collapse-to-count widget. SRS-059
SC-AAAV DR column is empty when no decision record affects the Controlled Item. SRS-058
SC-AAAW Broken Req-ID does not crash the build and the decision page is still rendered. SRS-056
SC-AAAX Only tables inside the Affected Documents section establish links.
SC-AAAY A backslash-escaped pipe inside a cell is a literal "|", not a column separator. SRS-052
SC-AAAZ Nested-folder decision page resolves Req-ID hrefs with correct relative depth. SRS-057
SC-AABA DR cell on the specification page resolves to a nested decision page. SRS-060

1.2 Source Code

# frozen_string_literal: true

require_relative 'spec_helper'

RSpec.describe 'Affected Documents', type: :aruba do
  context 'when a decision record has an Affected Documents section' do
    before do
      write_file('myproject/project.yml', "specifications:\n  input: []\n")
      write_file('myproject/specifications/req/req.md', <<~MD)
        # Requirements

        [REQ-001] A first requirement.

        [REQ-002] A second requirement.
      MD
      write_file('myproject/decisions/adr-300-affecting-req.md', <<~MD)
        ---
        title: "ADR-300: Affects REQ-001 and REQ-002"
        ---

        # Affected Documents

        | # | Proposed Text | Req-ID |
        |---|---|---|
        | 1 | A first requirement. | >[REQ-001] |
        | 2 | A second requirement. | >[REQ-002] |
      MD
      run_command_and_stop('almirah please myproject')
    end

    # <REQ> Recognises the Affected Documents section and parses its three-column table. >[SRS-052] >[SRS-053] </REQ>
    # <REQ> Req-ID column accepts >[BBB-NNN] reference syntax. >[SRS-054] </REQ>
    # <REQ> Render the Req-ID cell on the Decision page as a clickable link. >[SRS-057] </REQ>
    it 'renders Req-ID cells in the Affected Documents table as links to the Controlled Items' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/decisions/adr-300.html')))
      links = doc.css('a.external').select { |a| a['href']&.include?('specifications/req/req.html') }
      hrefs = links.map { |a| a['href'] }
      texts = links.map(&:text).map(&:strip)
      expect(hrefs).to include('./../specifications/req/req.html#REQ-001')
      expect(hrefs).to include('./../specifications/req/req.html#REQ-002')
      expect(texts).to include('REQ-001', 'REQ-002')
    end

    # <REQ> Controlled-paragraph table carries a DR column titled "Decision Record" after COV. >[SRS-058] </REQ>
    it 'renders the DR column header on the specification page after COV' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/specifications/req/req.html')))
      headers = doc.css('table.controlled thead th').map { |th| [th.text.strip, th['title']] }
      cov_index = headers.index { |text, _| text == 'COV' }
      dr_index = headers.index { |text, _| text == 'DR' }
      expect(cov_index).not_to be_nil
      expect(dr_index).to eq(cov_index + 1)
      expect(headers[dr_index][1]).to eq('Decision Record')
    end

    # <REQ> Establishes a link from each Affected Documents row to the referenced Item. >[SRS-055] </REQ>
    # <REQ> For each Controlled Item show the affecting Decision Records as clickable links. >[SRS-058] </REQ>
    # <REQ> Click on a decision record link navigates to the Decision Record page. >[SRS-060] </REQ>
    it 'renders a single decision-record link in the DR cell of each affected Controlled Item' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/specifications/req/req.html')))
      rows = doc.css('table.controlled tr')
      req1_row = rows.find { |tr| tr.css('td.item_id a[name="REQ-001"]').any? }
      req2_row = rows.find { |tr| tr.css('td.item_id a[name="REQ-002"]').any? }
      req1_dr = req1_row.css('td').last.css('a').first
      req2_dr = req2_row.css('td').last.css('a').first
      expect(req1_dr.text.strip).to eq('ADR-300')
      expect(req1_dr['href']).to eq('./../../decisions/adr-300.html')
      expect(req2_dr.text.strip).to eq('ADR-300')
      expect(req2_dr['href']).to eq('./../../decisions/adr-300.html')
    end
  end

  context 'when multiple decision records affect the same Controlled Item' do
    before do
      write_file('myproject/project.yml', "specifications:\n  input: []\n")
      write_file('myproject/specifications/req/req.md', <<~MD)
        # Requirements

        [REQ-001] A first requirement.
      MD
      write_file('myproject/decisions/adr-310-first.md', <<~MD)
        ---
        title: "ADR-310: First"
        ---

        # Affected Documents

        | # | Proposed Text | Req-ID |
        |---|---|---|
        | 1 | A first requirement. | >[REQ-001] |
      MD
      write_file('myproject/decisions/adr-311-second.md', <<~MD)
        ---
        title: "ADR-311: Second"
        ---

        # Affected Documents

        | # | Proposed Text | Req-ID |
        |---|---|---|
        | 1 | A first requirement (revised). | >[REQ-001] |
      MD
      run_command_and_stop('almirah please myproject')
    end

    # <REQ> Multi-link DR cell uses the collapse-to-count widget. >[SRS-059] </REQ>
    it 'collapses the DR cell to a clickable count with an expanded list' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/specifications/req/req.html')))
      collapsed = doc.at_css('div#DR_REQ-001 a')
      expanded = doc.css('div#DRS_REQ-001 a')
      expect(collapsed.text.strip).to eq('2')
      expect(collapsed['onclick']).to include('decisionLink_OnClick')
      expanded_texts = expanded.map(&:text).map(&:strip)
      expanded_hrefs = expanded.map { |a| a['href'] }
      expect(expanded_texts).to contain_exactly('ADR-310', 'ADR-311')
      expect(expanded_hrefs).to contain_exactly('./../../decisions/adr-310.html',
                                                './../../decisions/adr-311.html')
    end
  end

  context 'when a decision record has no Affected Documents section' do
    before do
      write_file('myproject/project.yml', "specifications:\n  input: []\n")
      write_file('myproject/specifications/req/req.md', <<~MD)
        # Requirements

        [REQ-001] A first requirement.
      MD
      write_file('myproject/decisions/adr-320-no-section.md', <<~MD)
        ---
        title: "ADR-320: No Affected Documents Section"
        ---

        ## Context

        A decision with no Affected Documents section.
      MD
      run_command_and_stop('almirah please myproject')
    end

    # <REQ> DR column is empty when no decision record affects the Controlled Item. >[SRS-058] </REQ>
    it 'renders the DR column header but with empty DR cells' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/specifications/req/req.html')))
      headers = doc.css('table.controlled thead th').map { |th| th.text.strip }
      expect(headers).to include('DR')
      row = doc.css('table.controlled tr').find { |tr| tr.css('td.item_id a[name="REQ-001"]').any? }
      expect(row.css('td').last.text.strip).to eq('')
    end
  end

  context 'when an Affected Documents row references a non-existing Controlled Item' do
    before do
      write_file('myproject/project.yml', "specifications:\n  input: []\n")
      write_file('myproject/specifications/req/req.md', <<~MD)
        # Requirements

        [REQ-001] A first requirement.
      MD
      write_file('myproject/decisions/adr-330-broken.md', <<~MD)
        ---
        title: "ADR-330: Broken Reference"
        ---

        # Affected Documents

        | # | Proposed Text | Req-ID |
        |---|---|---|
        | 1 | A non-existent requirement. | >[REQ-999] |
      MD
      run_command_and_stop('almirah please myproject')
    end

    # <REQ> Broken Req-ID does not crash the build and the decision page is still rendered. >[SRS-056] </REQ>
    it 'still renders the decision page when the Req-ID does not resolve' do
      expect(File.exist?(expand_path('myproject/build/decisions/adr-330.html'))).to be true
    end
  end

  context 'when a decision record has a sample table outside the Affected Documents section' do
    before do
      write_file('myproject/project.yml', "specifications:\n  input: []\n")
      write_file('myproject/specifications/req/req.md', <<~MD)
        # Requirements

        [REQ-001] A first requirement.
      MD
      write_file('myproject/decisions/adr-350-with-sample.md', <<~MD)
        ---
        title: "ADR-350: With sample table outside the section"
        ---

        # Decision

        Illustrative example of the format used in Affected Documents:

        | # | Proposed Text | Req-ID |
        |---|---|---|
        | 1 | sample row used only for illustration | >[REQ-001] |

        # Affected Documents

        | # | Proposed Text | Req-ID |
        |---|---|---|
        | 1 | A first requirement. | >[REQ-001] |
      MD
      run_command_and_stop('almirah please myproject')
    end

    # <REQ> Only tables inside the Affected Documents section establish links. >[SRS-052] >[SRS-055] </REQ>
    it 'ignores sample tables outside the Affected Documents section' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/specifications/req/req.html')))
      row = doc.css('table.controlled tr').find { |tr| tr.css('td.item_id a[name="REQ-001"]').any? }
      dr_cell = row.css('td').last
      single_link = dr_cell.css('a').first
      expect(dr_cell.css('div#DR_REQ-001').any?).to be false
      expect(single_link.text.strip).to eq('ADR-350')
      expect(single_link['href']).to eq('./../../decisions/adr-350.html')
    end
  end

  context 'when an Affected Documents row cell contains a backslash-escaped pipe' do
    before do
      write_file('myproject/project.yml', "specifications:\n  input: []\n")
      write_file('myproject/specifications/req/req.md', <<~MD)
        # Requirements

        [REQ-001] A first requirement.
      MD
      write_file('myproject/decisions/adr-360-escaped-pipe.md', <<~MD)
        ---
        title: "ADR-360: Escaped pipe in Proposed Text"
        ---

        # Affected Documents

        | # | Proposed Text | Req-ID |
        |---|---|---|
        | 1 | Support an alias `[[target\\|display text]]` rendering the display text. | >[REQ-001] |
      MD
      run_command_and_stop('almirah please myproject')
    end

    # <REQ> A backslash-escaped pipe inside a cell is a literal "|", not a column separator. >[SRS-052] </REQ>
    it 'keeps the row at three columns and renders the literal pipe' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/decisions/adr-360.html')))
      data_row = doc.css('table tr').find { |tr| tr.css('td a[name="adr-360.1"]').any? }
      expect(data_row).not_to be_nil
      expect(data_row.css('td').length).to eq(3)
      code = data_row.css('td code.inline').first
      expect(code.text).to eq('[[target|display text]]')
      ref_link = data_row.css('td').last.css('a').first
      expect(ref_link.text.strip).to eq('REQ-001')
    end
  end

  context 'when a decision record lives in a nested subfolder' do
    before do
      write_file('myproject/project.yml', "specifications:\n  input: []\n")
      write_file('myproject/specifications/req/req.md', <<~MD)
        # Requirements

        [REQ-001] A first requirement.
      MD
      write_file('myproject/decisions/issues/issue-340-nested.md', <<~MD)
        ---
        title: "ISSUE-340: Nested"
        ---

        # Affected Documents

        | # | Proposed Text | Req-ID |
        |---|---|---|
        | 1 | A first requirement. | >[REQ-001] |
      MD
      run_command_and_stop('almirah please myproject')
    end

    # <REQ> Nested-folder decision page resolves Req-ID hrefs with correct relative depth. >[SRS-057] </REQ>
    it 'renders the Req-ID link with the correct relative path from a nested decision page' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/decisions/issues/issue-340.html')))
      link = doc.css('a.external').find { |a| a['href']&.include?('REQ-001') }
      expect(link['href']).to eq('./../../specifications/req/req.html#REQ-001')
    end

    # <REQ> DR cell on the specification page resolves to a nested decision page. >[SRS-060] </REQ>
    it 'renders the DR cell href pointing at the nested decision page' do
      doc = Nokogiri::HTML(File.read(expand_path('myproject/build/specifications/req/req.html')))
      row = doc.css('table.controlled tr').find { |tr| tr.css('td.item_id a[name="REQ-001"]').any? }
      dr_link = row.css('td').last.css('a').first
      expect(dr_link.text.strip).to eq('ISSUE-340')
      expect(dr_link['href']).to eq('./../../decisions/issues/issue-340.html')
    end
  end
end