WillTools 2026-09-09 -- clean snapshot of the development repo for review
ci / test (push) Successful in 6m18s
ci / test (push) Successful in 6m18s
History-free snapshot of the private development repository at cbe45c5 (2026-09-09). Code, docs and public catalogue only; no plant or job knowledge layers, no client or job names (tools/scrub.py clean). The intro deck is left out of this snapshot because its slide images could not be checked for job drawings.
This commit is contained in:
@@ -0,0 +1,466 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""Host tests for `vibe.pa_extents` -- the Panel Analysis form's "Concrete
|
||||
Extents" section layout: panel geometry, the openings table, the drawings
|
||||
row, and Section Properties.
|
||||
|
||||
Runs on plain CPython via `tests/conftest.py`'s `vibe` stub -- this module
|
||||
imports no Revit and no `panelsketch` (it only NAMES a `panelsketch` entry
|
||||
point in a `drawing` block's `source`; the renderer, not this module, calls
|
||||
it). What this suite checks, per the task brief: only known block `kind`s
|
||||
come out; every `field` block carries a plain-English label and a hint;
|
||||
`openings_rows` being absent degrades to a `problems` entry rather than a
|
||||
crash; Section Properties never prints a row it did not derive; and `values`
|
||||
is never mutated.
|
||||
"""
|
||||
from vibe import panelanalysisform as paf # noqa: F401 (registers the stub)
|
||||
from vibe import pa_extents as E
|
||||
|
||||
|
||||
ALLOWED_KINDS = {'heading', 'columns', 'field', 'kv', 'sentence', 'table',
|
||||
'editable_table', 'drawing', 'rule', 'space'}
|
||||
|
||||
|
||||
def _walk(blocks):
|
||||
"""Yield every block in `blocks`, recursing into `columns`."""
|
||||
for b in blocks:
|
||||
yield b
|
||||
if b.get('kind') == 'columns':
|
||||
for col in b['columns']:
|
||||
for sub in _walk(col):
|
||||
yield sub
|
||||
|
||||
|
||||
def _all_field_blocks(blocks):
|
||||
return [b for b in _walk(blocks) if b.get('kind') == 'field']
|
||||
|
||||
|
||||
def _all_drawing_blocks(blocks):
|
||||
return [b for b in _walk(blocks) if b.get('kind') == 'drawing']
|
||||
|
||||
|
||||
# --------------------------------------------------------------- vocabulary
|
||||
|
||||
def test_empty_values_only_known_kinds_no_crash():
|
||||
blocks, problems = E.page({})
|
||||
assert isinstance(blocks, list)
|
||||
assert isinstance(problems, list)
|
||||
for b in _walk(blocks):
|
||||
assert b['kind'] in ALLOWED_KINDS, 'unknown kind: {0!r}'.format(b)
|
||||
|
||||
|
||||
def test_none_values_does_not_raise():
|
||||
# page() must never raise -- an absent values dict is a valid call.
|
||||
blocks, problems = E.page(None)
|
||||
assert isinstance(blocks, list)
|
||||
assert isinstance(problems, list)
|
||||
|
||||
|
||||
def test_filled_values_only_known_kinds_no_crash():
|
||||
values = {
|
||||
'length_ft': '107', 'width_ft': '9', 'thickness_in': '8',
|
||||
'section_type': 'Solid', 'top_in_form': 'Interior',
|
||||
'top_elevation_ft': '107', 'bottom_elevation_ft': '0',
|
||||
'left_miter': 'no', 'right_miter': 'yes',
|
||||
'openings_rows': [
|
||||
{'type': 'opening', 'pos_along': 8.0 / 12.0,
|
||||
'pos_along_from': 'start', 'length': 6.875,
|
||||
'pos_cross': 2 + 11.5 / 12.0, 'pos_cross_from': 'left',
|
||||
'width': 3 + 4.5 / 12.0, 'pos_vert': None, 'height': None,
|
||||
'label': ''},
|
||||
],
|
||||
}
|
||||
blocks, problems = E.page(values)
|
||||
for b in _walk(blocks):
|
||||
assert b['kind'] in ALLOWED_KINDS
|
||||
|
||||
|
||||
# ------------------------------------------------------------ field quality
|
||||
|
||||
def test_every_field_has_a_plain_label_and_a_hint():
|
||||
blocks, _problems = E.page({})
|
||||
fields = _all_field_blocks(blocks)
|
||||
assert fields, 'expected at least one field block'
|
||||
for f in fields:
|
||||
assert f['label'] and f['label'].strip(), \
|
||||
'field {0!r} has no plain-English label'.format(f['key'])
|
||||
assert f['hint'] and f['hint'].strip(), \
|
||||
'field {0!r} has no hint'.format(f['key'])
|
||||
assert f['editor'] in ('text', 'num', 'check', 'choice')
|
||||
assert isinstance(f['required'], bool)
|
||||
|
||||
|
||||
def test_known_paf_fields_use_panelanalysisform_label_verbatim():
|
||||
# Checked LIVE against panelanalysisform.FIELDS rather than a hardcoded
|
||||
# key list, because a sibling agent is restructuring that module WHILE
|
||||
# this suite runs -- `left_miter` / `right_miter` landed there mid-task
|
||||
# (see the module docstring's "read defensively" discipline). Any key
|
||||
# `_paf_field` finds must be reflected verbatim; any key it does not
|
||||
# find is covered by the MISSING test below instead.
|
||||
blocks, _problems = E.page({})
|
||||
fields = {f['key']: f for f in _all_field_blocks(blocks)}
|
||||
candidates = ('length_ft', 'width_ft', 'thickness_in', 'section_type',
|
||||
'top_in_form', 'left_miter', 'right_miter',
|
||||
'top_elevation_ft', 'bottom_elevation_ft')
|
||||
for key in candidates:
|
||||
if key not in fields:
|
||||
continue
|
||||
if E._paf_field(key) is None:
|
||||
continue # MISSING from panelanalysisform -- not this test's job
|
||||
assert fields[key]['label'] == paf.label_of(key), \
|
||||
'{0} EXISTS in panelanalysisform but label was not taken from it' \
|
||||
.format(key)
|
||||
assert fields[key]['hint'] == (paf.help_of(key) or fields[key]['hint'])
|
||||
|
||||
|
||||
def test_previously_missing_elevation_keys_now_declared_and_rendered():
|
||||
# 2026-09-05: `top_elevation_ft` / `bottom_elevation_ft` are real
|
||||
# `panelanalysisform.FIELDS` entries (they used to be MISSING, which is
|
||||
# what the deleted predecessor of this test pinned) -- a value typed
|
||||
# into either is saved, and `_field` no longer prints a "not a field
|
||||
# yet" limitation note for them.
|
||||
blocks, problems = E.page({})
|
||||
fields = {f['key']: f for f in _all_field_blocks(blocks)}
|
||||
joined = ' '.join(problems)
|
||||
for key in ('top_elevation_ft', 'bottom_elevation_ft'):
|
||||
assert E._paf_field(key) is not None, \
|
||||
'{0} should be declared in panelanalysisform.FIELDS'.format(key)
|
||||
assert key in fields
|
||||
assert repr(key) not in joined, \
|
||||
'{0} is declared but still named as missing in problems' \
|
||||
.format(key)
|
||||
|
||||
|
||||
def test_formed_step_controls_stay_declared_but_are_not_rendered():
|
||||
# Tombstone (issue #22, item 4, 2026-09-07). `top_in_form_check` /
|
||||
# `top_in_form_width_in` / `top_in_form_height_in` are Eriksson's "top
|
||||
# in form" STEP; nothing reads them (not `pa_sketch_extents`, not the
|
||||
# weight/centroid summary, not `panellift`), and the owner's complaint
|
||||
# was a checkbox plus two boxes that accepted input and changed
|
||||
# nothing. They stay DECLARED (a saved value survives) and are NOT
|
||||
# rendered until something consumes them -- and the hint says so.
|
||||
blocks, problems = E.page({'top_in_form_check': 'yes',
|
||||
'top_in_form_width_in': '2',
|
||||
'top_in_form_height_in': '3'})
|
||||
fields = {f['key']: f for f in _all_field_blocks(blocks)}
|
||||
for key in ('top_in_form_check', 'top_in_form_width_in',
|
||||
'top_in_form_height_in'):
|
||||
assert E._paf_field(key) is not None, \
|
||||
'{0} must stay declared in panelanalysisform.FIELDS'.format(key)
|
||||
assert key not in fields, \
|
||||
'{0} is rendered again but still consumed by nothing'.format(key)
|
||||
assert 'not shown on the page' in paf.help_of(key).lower()
|
||||
assert repr(key) not in ' '.join(problems)
|
||||
|
||||
|
||||
def test_face_up_field_is_visible_only_for_a_sandwich_section():
|
||||
# Owner, 2026-09-07 (issue #22, item 3): "section type = solid 的时候,
|
||||
# which face is up the casting bed 不应该存在". The block carries the
|
||||
# `visible_when` contract the page honours live; the field itself is
|
||||
# still emitted (its value is kept) -- hiding is the page's job.
|
||||
blocks, _problems = E.page({'section_type': 'Solid'})
|
||||
fields = {f['key']: f for f in _all_field_blocks(blocks)}
|
||||
assert fields['top_in_form']['visible_when'] == {
|
||||
'key': 'section_type', 'in': ['Sandwich']}
|
||||
assert 'visible_when' not in fields['length_ft']
|
||||
|
||||
|
||||
def test_length_ft_is_required_and_matches_paf():
|
||||
blocks, _problems = E.page({})
|
||||
fields = {f['key']: f for f in _all_field_blocks(blocks)}
|
||||
assert fields['length_ft']['required'] is True
|
||||
assert fields['width_ft']['required'] is True
|
||||
assert fields['thickness_in']['required'] is True
|
||||
|
||||
|
||||
# ---------------------------------------------------------------- miters
|
||||
|
||||
def test_miter_blank_prints_no_sentence():
|
||||
blocks, _problems = E.page({})
|
||||
sentences = [b['text'] for b in _walk(blocks) if b['kind'] == 'sentence']
|
||||
assert not any('miter not included' in s for s in sentences)
|
||||
|
||||
|
||||
def test_miter_explicit_no_prints_a_sentence_not_a_blank():
|
||||
blocks, _problems = E.page({'left_miter': 'no'})
|
||||
sentences = [b['text'] for b in _walk(blocks) if b['kind'] == 'sentence']
|
||||
assert any('Left miter not included' in s for s in sentences)
|
||||
|
||||
|
||||
def test_miter_explicit_yes_prints_no_sentence():
|
||||
blocks, _problems = E.page({'right_miter': 'yes'})
|
||||
sentences = [b['text'] for b in _walk(blocks) if b['kind'] == 'sentence']
|
||||
assert not any('Right miter not included' in s for s in sentences)
|
||||
|
||||
|
||||
def test_miter_sentences_are_quiet_2026_09_04():
|
||||
# Owner ruling, 2026-09-04: the miter sentences stay `sentence` blocks
|
||||
# (already rendered grey/italic by `script.py`'s `_render_sentence`) but
|
||||
# now carry `'quiet': True`, distinguishing a footnote beside a field
|
||||
# from the openings summary sentence -- the other `sentence` block this
|
||||
# page emits, deliberately left un-quiet.
|
||||
blocks, _problems = E.page({'left_miter': 'no', 'right_miter': 'no'})
|
||||
quiet_texts = [b['text'] for b in _walk(blocks)
|
||||
if b['kind'] == 'sentence' and b.get('quiet') is True]
|
||||
assert any('Left miter not included' in s for s in quiet_texts)
|
||||
assert any('Right miter not included' in s for s in quiet_texts)
|
||||
summary = [b for b in _walk(blocks) if b['kind'] == 'sentence'
|
||||
and 'openings' in b['text'].lower()]
|
||||
assert summary and not any(b.get('quiet') for b in summary)
|
||||
|
||||
|
||||
def test_miters_are_choice_editors_not_checkboxes():
|
||||
"""2026-09-04: a plain CheckBox can only write 'yes' or '' (see
|
||||
`script.py`'s `current_values`), so it could never produce the explicit
|
||||
'no' `_miter_sentence` looks for -- the miter fields are an EDITABLE
|
||||
Yes/No dropdown instead, so blank/Yes/No all round-trip as real text."""
|
||||
blocks, _problems = E.page({})
|
||||
fields = {f['key']: f for f in _all_field_blocks(blocks)}
|
||||
for key in ('left_miter', 'right_miter'):
|
||||
assert fields[key]['editor'] == 'choice'
|
||||
assert fields[key]['choices'] == ['Yes', 'No']
|
||||
|
||||
|
||||
# -------------------------------------------------------------- openings
|
||||
#
|
||||
# Owner ruling, 2026-09-05: the Openings/Reveals/Solid Zones table is now an
|
||||
# EDITABLE ROW TABLE (`kind: 'editable_table'`, `table: 'opening'`) -- always
|
||||
# present, regardless of what `values['openings_rows']` (the legacy read-only
|
||||
# source) carries, since an editable table always has somewhere to add a
|
||||
# first row. The tests this replaces pinned the old read-only grouped-table
|
||||
# behaviour (a `problems` entry when `openings_rows` was absent, a `table`
|
||||
# block with Eriksson's own 7-column layout) -- deleted rather than kept
|
||||
# alongside, since there is no successor invariant for either.
|
||||
|
||||
def _opening_table_block(blocks):
|
||||
return next(b for b in _walk(blocks)
|
||||
if b.get('kind') == 'editable_table'
|
||||
and b.get('table') == 'opening')
|
||||
|
||||
|
||||
def test_opening_table_always_present_regardless_of_legacy_openings_rows():
|
||||
for values in ({}, {'openings': ''}, {'openings_rows': []},
|
||||
{'openings_rows': [{'type': 'Opening'}]}):
|
||||
blocks, _problems = E.page(values)
|
||||
block = _opening_table_block(blocks)
|
||||
assert block['columns'][0]['col'] == 'type'
|
||||
assert set(c['col'] for c in block['columns']) == set(
|
||||
['type', 'x_ft', 'length_ft', 'y_ft', 'width_ft', 'depth_in'])
|
||||
|
||||
|
||||
def test_opening_table_type_column_offers_the_three_kinds():
|
||||
blocks, _problems = E.page({})
|
||||
block = _opening_table_block(blocks)
|
||||
type_col = next(c for c in block['columns'] if c['col'] == 'type')
|
||||
assert type_col['editor'] == 'choice'
|
||||
assert type_col['choices'] == ['Opening', 'Reveal', 'Solid zone']
|
||||
|
||||
|
||||
def test_no_read_only_openings_table_kind_left_on_the_page():
|
||||
# The OLD grouped read-only 'table' kind is gone from this section --
|
||||
# only the new editable_table remains for openings.
|
||||
blocks, _problems = E.page({'openings_rows': [{'type': 'Opening'}]})
|
||||
assert not any(b.get('kind') == 'table' for b in _walk(blocks))
|
||||
|
||||
|
||||
# ------------------------------------------------------------ section props
|
||||
#
|
||||
# The owner ruled, 2026-09-03, that Concrete Extents drops the Section
|
||||
# Properties parameter dump (Area/Width/Height/Cx/Cy/Izz/Iyy/Izy/Sb/St)
|
||||
# entirely -- no kv block, ever, regardless of what inputs are given. The
|
||||
# three tests that used to live here (`test_section_properties_never_
|
||||
# invents_a_derived_row`, `test_section_properties_values_are_correct`,
|
||||
# `test_section_properties_missing_inputs_never_prints_a_kv_block`) pinned
|
||||
# that removed feature -- one of them even asserted a kv block MUST appear
|
||||
# once width+thickness are given, which is now false unconditionally -- and
|
||||
# are deleted rather than rewritten: there is no successor invariant to carry
|
||||
# forward, because the page has no Section Properties concept left to test.
|
||||
# `test_drawings_are_the_stacked_full_width_pa_sketch_extents_pair` below
|
||||
# already covers "no kv block, ever" for the new page.
|
||||
|
||||
# -------------------------------------------------------------- drawings
|
||||
#
|
||||
# 2026-09-04: the outline/section pair is no longer a `columns`-wrapped,
|
||||
# fixed-pixel pair naming `panelsketch.extents_outline` / `extents_section`
|
||||
# directly. The owner ruled the outline drawn bigger, the section moved
|
||||
# BELOW it (not beside it), both `full_width`, and a draggable cut line on
|
||||
# the outline that the section redraws around -- see `pa_extents.py`'s "WHY
|
||||
# THE TWO DRAWINGS ARE NOW STACKED" docstring note. `test_drawings_use_only_
|
||||
# named_panelsketch_entry_points` (the old invariant, pinned to `source in
|
||||
# {'extents_outline', 'extents_section'}` plus integer `w`/`h`) is deleted
|
||||
# rather than rewritten -- there is no `w`/`h` any more, and `source` now
|
||||
# names an entry point in `pa_sketch_extents`, not `panelsketch` directly.
|
||||
|
||||
_DRAWING_ORDER = ['outline', 'section']
|
||||
|
||||
|
||||
def test_drawings_are_the_stacked_full_width_pa_sketch_extents_pair():
|
||||
blocks, _problems = E.page({})
|
||||
drawings = _all_drawing_blocks(blocks)
|
||||
assert [d['source'] for d in drawings] == _DRAWING_ORDER
|
||||
for d in drawings:
|
||||
assert d['module'] == 'pa_sketch_extents'
|
||||
assert d['full_width'] is True
|
||||
assert isinstance(d['aspect'], float)
|
||||
assert 0.0 < d['aspect'] <= 1.0
|
||||
assert d['caption'] and d['caption'].strip()
|
||||
assert isinstance(d['depends_on'], list) and d['depends_on']
|
||||
assert not any(b['kind'] == 'kv' for b in _walk(blocks))
|
||||
|
||||
|
||||
def test_outline_block_carries_the_cutline_contract_the_section_does_not():
|
||||
blocks, _problems = E.page({})
|
||||
drawings = {d['source']: d for d in _all_drawing_blocks(blocks)}
|
||||
assert drawings['outline']['cutline'] == {'key': '_cut_frac',
|
||||
'default': 0.5}
|
||||
assert 'cutline' not in drawings['section']
|
||||
|
||||
|
||||
def test_outline_and_section_depends_on_match_the_contract_exactly():
|
||||
# These lists are read by NAME on the renderer's side of the contract --
|
||||
# see `pa_extents.py`'s `_extents_drawing_blocks()` docstring. Pinned
|
||||
# verbatim so a future edit that drops or renames a key is caught here,
|
||||
# not by a redraw that silently stops happening in the live form.
|
||||
blocks, _problems = E.page({})
|
||||
drawings = {d['source']: d for d in _all_drawing_blocks(blocks)}
|
||||
# 'opening' (2026-09-05, the ROW TABLE prefix) leads the list now --
|
||||
# `redrawDependents` treats a `depends_on` entry as an exact match OR a
|
||||
# `key + '.'` PREFIX match, so this one entry watches every cell of
|
||||
# every opening row; 'openings_rows' stays for a saved panel with no
|
||||
# table rows at all.
|
||||
assert drawings['outline']['depends_on'] == [
|
||||
'opening', 'openings_rows', 'length_ft', 'width_ft']
|
||||
assert drawings['section']['depends_on'] == [
|
||||
'_cut_frac', 'opening', 'openings_rows', 'thickness_in', 'width_ft',
|
||||
'section_type', 'wythe_top_in', 'insulation_in', 'wythe_bottom_in']
|
||||
|
||||
|
||||
def test_outline_aspect_is_width_over_length_clamped():
|
||||
# 4 / 40 = 0.1, below the 0.12 floor -- clamped up so a very long, thin
|
||||
# panel does not collapse the outline's canvas to a sliver. Then +0.10,
|
||||
# the fixed allowance `pa_sketch_extents.outline()` needs above/below the
|
||||
# panel rectangle for the title/cut label and the L label/dimension
|
||||
# chain (2026-09-04 live-render finding -- see `_OUTLINE_LABEL_ROWS_ASPECT`).
|
||||
blocks, _problems = E.page({'length_ft': '40', 'width_ft': '4'})
|
||||
drawings = {d['source']: d for d in _all_drawing_blocks(blocks)}
|
||||
assert abs(drawings['outline']['aspect'] - 0.22) < 1e-9
|
||||
# 30 / 40 = 0.75, above the 0.5 ceiling -- clamped down so a nearly
|
||||
# square panel does not blow the outline's canvas up to a near-square.
|
||||
# Then +0.10, same allowance.
|
||||
blocks, _problems = E.page({'length_ft': '40', 'width_ft': '30'})
|
||||
drawings = {d['source']: d for d in _all_drawing_blocks(blocks)}
|
||||
assert abs(drawings['outline']['aspect'] - 0.6) < 1e-9
|
||||
# 8 / 40 = 0.2, inside the clamp -- passed through, then +0.10.
|
||||
blocks, _problems = E.page({'length_ft': '40', 'width_ft': '8'})
|
||||
drawings = {d['source']: d for d in _all_drawing_blocks(blocks)}
|
||||
assert abs(drawings['outline']['aspect'] - 0.3) < 1e-9
|
||||
|
||||
|
||||
def test_section_aspect_is_thickness_over_width_clamped():
|
||||
# thickness_ft = 4/12; width_ft = 20 -> ratio ~0.0167, below the 0.12
|
||||
# floor -- clamped up.
|
||||
blocks, _problems = E.page(
|
||||
{'length_ft': '24', 'width_ft': '20', 'thickness_in': '4'})
|
||||
drawings = {d['source']: d for d in _all_drawing_blocks(blocks)}
|
||||
assert abs(drawings['section']['aspect'] - 0.12) < 1e-9
|
||||
|
||||
|
||||
def test_aspects_default_when_geometry_is_missing():
|
||||
blocks, _problems = E.page({})
|
||||
drawings = {d['source']: d for d in _all_drawing_blocks(blocks)}
|
||||
# outline carries the +0.10 label-rows allowance on top of the clamped
|
||||
# ratio's own [0.12, 0.5] range -- see `_OUTLINE_LABEL_ROWS_ASPECT`.
|
||||
assert 0.22 <= drawings['outline']['aspect'] <= 0.6
|
||||
assert 0.12 <= drawings['section']['aspect'] <= 0.35
|
||||
|
||||
|
||||
def test_geometry_then_openings_table_then_outline_then_section():
|
||||
"""2026-09-04 layout ruling: geometry fields, then the openings table,
|
||||
then the outline, then the section, each full width -- pinning the
|
||||
ORDER blocks come out in, not just which ones exist. 2026-09-05: the
|
||||
openings table is now `editable_table`, not `table`."""
|
||||
blocks, _problems = E.page(
|
||||
{'length_ft': '24', 'width_ft': '8', 'thickness_in': '8',
|
||||
'openings_rows': []})
|
||||
kinds_and_sources = []
|
||||
for b in blocks:
|
||||
if b['kind'] in ('heading', 'table', 'editable_table'):
|
||||
kinds_and_sources.append(b['kind'])
|
||||
elif b['kind'] == 'columns':
|
||||
kinds_and_sources.append('columns')
|
||||
elif b['kind'] == 'drawing':
|
||||
kinds_and_sources.append('drawing:' + b['source'])
|
||||
# heading (Concrete Extents), heading+columns (Panel Geometry),
|
||||
# heading (Openings...), editable_table, drawing:outline, drawing:section
|
||||
assert kinds_and_sources == [
|
||||
'heading', 'heading', 'columns', 'heading', 'editable_table',
|
||||
'drawing:outline', 'drawing:section']
|
||||
|
||||
|
||||
# ------------------------------------------------------------ no mutation
|
||||
|
||||
def test_values_dict_is_never_mutated():
|
||||
values = {'length_ft': '107', 'width_ft': '9', 'thickness_in': '8',
|
||||
'openings': 'garbage'}
|
||||
before = dict(values)
|
||||
E.page(values)
|
||||
assert values == before
|
||||
|
||||
|
||||
def test_page_never_raises_on_garbage_values():
|
||||
garbage = {
|
||||
'width_ft': 'not-a-number', 'thickness_in': [1, 2, 3],
|
||||
'openings_rows': 'not-a-list', 'left_miter': object(),
|
||||
'openings': None,
|
||||
}
|
||||
blocks, problems = E.page(garbage)
|
||||
assert isinstance(blocks, list)
|
||||
assert isinstance(problems, list)
|
||||
|
||||
|
||||
# --------------------------------------------------------------- utilities
|
||||
|
||||
def test_feet_str_formats_like_eriksson():
|
||||
assert E._feet_str(8.0 / 12.0) == '8"'
|
||||
assert E._feet_str(0.0) == '0"'
|
||||
assert E._feet_str(20.0 + 8.0 / 12.0) == "20'-8\""
|
||||
assert E._feet_str(2.0 + 11.5 / 12.0) == "2'-11 1/2\""
|
||||
|
||||
|
||||
def test_dim_or_dash_passes_through_preformatted_strings():
|
||||
assert E._dim_or_dash("2'-11.5\"") == "2'-11.5\""
|
||||
assert E._dim_or_dash('') == '-'
|
||||
assert E._dim_or_dash(None) == '-'
|
||||
|
||||
|
||||
def test_tri_bool_blank_is_none_not_false():
|
||||
assert E._tri_bool('') is None
|
||||
assert E._tri_bool(None) is None
|
||||
assert E._tri_bool('no') is False
|
||||
assert E._tri_bool('yes') is True
|
||||
|
||||
|
||||
# `test_drawings_are_the_new_extents_outline_and_section_pair` (the
|
||||
# 2026-09-03 side-by-side `columns` pair naming `panelsketch.extents_outline`
|
||||
# / `extents_section` directly) is deleted, not rewritten -- the 2026-09-04
|
||||
# stacked, full-width, `pa_sketch_extents`-naming pair it pinned is now
|
||||
# covered by `test_drawings_are_the_stacked_full_width_pa_sketch_extents_pair`
|
||||
# and its neighbours above.
|
||||
|
||||
|
||||
def test_one_opening_produces_the_single_summary_sentence():
|
||||
values = {'length_ft': '24', 'width_ft': '8', 'thickness_in': '8',
|
||||
'openings_rows': [
|
||||
{'type': 'Opening', 'pos_along': 10, 'pos_along_from': 'start',
|
||||
'length': 4, 'pos_cross': 1, 'pos_cross_from': 'left',
|
||||
'width': 3, 'pos_vert': 2, 'height': 3, 'label': 'DOOR'},
|
||||
]}
|
||||
blocks, _problems = E.page(values)
|
||||
sentences = [b['text'] for b in _walk(blocks) if b['kind'] == 'sentence']
|
||||
assert '1 opening, 0 reveals.' in sentences
|
||||
|
||||
|
||||
def test_no_openings_produces_the_no_openings_sentence():
|
||||
blocks, _problems = E.page({'length_ft': '24', 'width_ft': '8',
|
||||
'thickness_in': '8', 'openings_rows': []})
|
||||
sentences = [b['text'] for b in _walk(blocks) if b['kind'] == 'sentence']
|
||||
assert 'No openings or reveals.' in sentences
|
||||
Reference in New Issue
Block a user