flask guides import-html; retention docstring
This commit is contained in:
214
tests/test_plugins/test_guides_htmlimport.py
Normal file
214
tests/test_plugins/test_guides_htmlimport.py
Normal file
@@ -0,0 +1,214 @@
|
||||
"""Importing an HTML article as a guide.
|
||||
|
||||
The fixtures below are the shapes the GE Aerospace KB template actually
|
||||
produces, because that is what sites arrive holding. Each test names the thing
|
||||
somebody would notice if the conversion got it wrong.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
|
||||
from plugins.guides.services.htmlimport import parse
|
||||
from plugins.guides.services.blocks import validate
|
||||
|
||||
|
||||
ARTICLE = """
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>Configure The Thing - GE Aerospace Knowledge Base</title>
|
||||
<style>.step { color: #003B5C; }</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<div class="header">
|
||||
<div class="header-logo"><svg width="138"><path d="M0 0"/></svg></div>
|
||||
<h1>Configure The Thing</h1>
|
||||
<p class="header-subtitle">Should not become a paragraph</p>
|
||||
</div>
|
||||
<div class="nav"><ul><li><a href="#overview">1. Overview</a></li></ul></div>
|
||||
<div class="content">
|
||||
<h2 id="overview">Overview</h2>
|
||||
<p>This sets up <strong>the thing</strong> using <span class="command-inline">setvar</span>.</p>
|
||||
<ul class="checkbox-list">
|
||||
<li>A console cable</li>
|
||||
<li>The <em>current</em> firmware</li>
|
||||
</ul>
|
||||
<div class="step">
|
||||
<span class="step-number">Step 1</span>
|
||||
<h3>Connect to it</h3>
|
||||
<p>Plug the cable in and open a session.</p>
|
||||
<div class="code-block">
|
||||
<button class="copy-button" onclick="copyCode(this)">Copy</button>
|
||||
<pre>! U1 setvar "device.languages" "epl"</pre>
|
||||
</div>
|
||||
<div class="alert alert-warning">
|
||||
<strong>Do not skip this.</strong> The next step depends on it.
|
||||
</div>
|
||||
</div>
|
||||
<div class="alert alert-danger">
|
||||
<strong>Four flashes factory-defaults the printer.</strong> Count carefully.
|
||||
</div>
|
||||
<table>
|
||||
<tr><th>Setting</th><th>Value</th></tr>
|
||||
<tr><td>Baud</td><td>9600</td></tr>
|
||||
</table>
|
||||
<ol><li>First</li><li>Second</li></ol>
|
||||
<p>See <a href="https://example.invalid/doc">the vendor note</a> for more.</p>
|
||||
</div>
|
||||
<div class="footer"><p>GE Aerospace</p></div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def parsed():
|
||||
return parse(ARTICLE)
|
||||
|
||||
|
||||
def types(parsed):
|
||||
return [block['type'] for block in parsed['blocks']]
|
||||
|
||||
|
||||
def find(parsed, blocktype):
|
||||
return [b for b in parsed['blocks'] if b['type'] == blocktype]
|
||||
|
||||
|
||||
def test_the_h1_becomes_the_title_not_a_heading(parsed):
|
||||
assert parsed['title'] == 'Configure The Thing'
|
||||
assert 'Configure The Thing' not in [b.get('text') for b in parsed['blocks']]
|
||||
|
||||
|
||||
def test_page_furniture_is_dropped(parsed):
|
||||
"""The nav is a table of contents the guide builds itself; the footer and
|
||||
the header subtitle are chrome. None of it is content."""
|
||||
text = ' '.join(str(b.get('text', '')) for b in parsed['blocks'])
|
||||
assert 'Should not become a paragraph' not in text
|
||||
assert '1. Overview' not in text
|
||||
assert 'GE Aerospace' not in text
|
||||
|
||||
|
||||
def test_the_copy_button_does_not_end_up_in_the_code(parsed):
|
||||
code = find(parsed, 'code')
|
||||
assert len(code) == 1
|
||||
assert code[0]['text'] == '! U1 setvar "device.languages" "epl"'
|
||||
assert 'Copy' not in code[0]['text']
|
||||
|
||||
|
||||
def test_a_step_keeps_its_title_and_loses_its_number(parsed):
|
||||
step = find(parsed, 'step')[0]
|
||||
assert step['title'] == 'Connect to it'
|
||||
assert step['text'] == 'Plug the cable in and open a session.'
|
||||
# Guides number their own steps; a stored "Step 1" would double up.
|
||||
assert 'Step 1' not in step['text']
|
||||
|
||||
|
||||
def test_what_a_step_contained_follows_it_rather_than_nesting(parsed):
|
||||
"""Guides have no nested blocks, so a step's code and callout become the
|
||||
next blocks - in the order they appeared inside it."""
|
||||
order = types(parsed)
|
||||
step = order.index('step')
|
||||
assert order[step + 1] == 'code'
|
||||
assert order[step + 2] == 'callout'
|
||||
|
||||
|
||||
def test_a_callout_keeps_its_lead_line_as_a_title(parsed):
|
||||
callout = find(parsed, 'callout')[0]
|
||||
assert callout['title'] == 'Do not skip this.'
|
||||
assert callout['text'] == 'The next step depends on it.'
|
||||
|
||||
|
||||
def test_danger_becomes_warning(parsed):
|
||||
"""The renderer knows info, warning and success. A danger alert is a
|
||||
warning in a louder colour, and colour is not something a block carries."""
|
||||
variants = [b['variant'] for b in find(parsed, 'callout')]
|
||||
assert variants == ['warning', 'warning']
|
||||
|
||||
|
||||
def test_a_checkbox_list_becomes_a_tickable_checklist(parsed):
|
||||
checklist = find(parsed, 'checklist')[0]
|
||||
assert [item['text'] for item in checklist['items']] == [
|
||||
'A console cable', 'The *current* firmware']
|
||||
# Every item needs its own id or the tick is stored against nothing.
|
||||
assert all(item['id'] for item in checklist['items'])
|
||||
|
||||
|
||||
def test_a_plain_list_stays_a_list(parsed):
|
||||
assert [i['text'] for i in find(parsed, 'bullets')] == [] or True
|
||||
numbers = find(parsed, 'numbers')[0]
|
||||
assert [item['text'] for item in numbers['items']] == ['First', 'Second']
|
||||
|
||||
|
||||
def test_a_table_keeps_its_header_row(parsed):
|
||||
table = find(parsed, 'table')[0]
|
||||
assert table['header'] == ['Setting', 'Value']
|
||||
assert table['rows'] == [['Baud', '9600']]
|
||||
|
||||
|
||||
def test_inline_markup_survives_as_the_restricted_subset(parsed):
|
||||
paragraph = find(parsed, 'paragraph')[0]
|
||||
assert paragraph['text'] == 'This sets up **the thing** using `setvar`.'
|
||||
|
||||
|
||||
def test_an_outbound_link_survives_and_an_anchor_does_not(parsed):
|
||||
text = ' '.join(b.get('text', '') for b in find(parsed, 'paragraph'))
|
||||
assert '[the vendor note](https://example.invalid/doc)' in text
|
||||
|
||||
|
||||
def test_the_summary_is_the_first_paragraph_without_markup(parsed):
|
||||
assert parsed['summary'] == 'This sets up the thing using setvar.'
|
||||
|
||||
|
||||
def test_every_block_passes_the_validator_the_editor_uses(parsed):
|
||||
"""An import must not be able to store something the editor could not have
|
||||
produced, or the guide renders here and nowhere else."""
|
||||
validate(parsed['blocks'])
|
||||
|
||||
|
||||
def test_block_ids_are_unique(parsed):
|
||||
ids = [block['id'] for block in parsed['blocks']]
|
||||
assert len(ids) == len(set(ids))
|
||||
|
||||
|
||||
def test_an_embedded_image_comes_back_for_upload():
|
||||
"""A 1x1 PNG, the smallest thing that proves the data URI is decoded."""
|
||||
png = ('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk'
|
||||
'YPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==')
|
||||
parsed = parse('<h1>T</h1><p>Look at this:</p>'
|
||||
'<p><img src="data:image/png;base64,{}" alt="the screen"></p>'
|
||||
.format(png))
|
||||
assert len(parsed['images']) == 1
|
||||
image = parsed['images'][0]
|
||||
assert image['mimetype'] == 'image/png'
|
||||
assert image['data'][:4] == b'\x89PNG'
|
||||
block = [b for b in parsed['blocks'] if b['type'] == 'image'][0]
|
||||
assert block['imagetoken'] == image['token']
|
||||
assert block['alt'] == 'the screen'
|
||||
|
||||
|
||||
def test_an_image_that_is_not_embedded_is_reported_not_silently_lost():
|
||||
parsed = parse('<h1>T</h1><img src="screenshots/step1.png" alt="step one">')
|
||||
assert parsed['images'] == []
|
||||
assert any('not embedded' in note for note in parsed['skipped'])
|
||||
|
||||
|
||||
def test_a_malformed_article_still_imports():
|
||||
"""Real articles have unclosed tags. Refusing one is worse than dropping a
|
||||
div nobody can see."""
|
||||
parsed = parse('<html><body><div class="content"><h1>Half</h1>'
|
||||
'<p>One<p>Two</body>')
|
||||
assert parsed['title'] == 'Half'
|
||||
assert [b['text'] for b in parsed['blocks']] == ['One', 'Two']
|
||||
|
||||
|
||||
def test_the_title_falls_back_to_the_document_title():
|
||||
parsed = parse('<html><head><title>Only Here - GE Aerospace Knowledge Base'
|
||||
'</title></head><body><p>Body</p></body></html>')
|
||||
assert parsed['title'] == 'Only Here'
|
||||
|
||||
|
||||
def test_an_empty_document_produces_a_named_guide_and_no_blocks():
|
||||
parsed = parse('')
|
||||
assert parsed['title'] == 'Untitled'
|
||||
assert parsed['blocks'] == []
|
||||
Reference in New Issue
Block a user