Put firmware selections first and show contextual board notes
This commit is contained in:
1 parent
3d539b0a8d
commit
ff78f8df7e
6 files changed
+324
-105
No files matched your search
@@ -2127,6 +2127,94 @@ assert.strictEqual(JSON.stringify(feedbackCatalog), feedbackSnapshot);
|
||||
assert.deepStrictEqual(simpleFilters, {...feedbackFilters, hardwareFamily: 'Heltec_v3'});
|
||||
console.log('same-image selectors skip redundant steps while distinct profiles and install routes stay explicit');
|
||||
|
||||
// Board-specific help belongs to exact qualified results, never to a broad
|
||||
// family filter or an unrelated role which happens to share the board name.
|
||||
const boardText = profile => picker.boardNotes(profile).join('\n');
|
||||
const boardProfile = target => currentCatalog.profiles.find(profile => profile.target === target);
|
||||
assert.deepStrictEqual(picker.boardNotes(null), []);
|
||||
assert.deepStrictEqual(picker.boardNotes({}), []);
|
||||
for (const board of ['RAK_3401', 'RAK_4631']) {
|
||||
for (const policy of ['full', 'reduced']) {
|
||||
const profile = boardProfile(board + '_repeater_unified_lora_ota-' + policy);
|
||||
assert(profile);
|
||||
const before = JSON.stringify(profile);
|
||||
const notes = boardText(profile);
|
||||
assert(notes.includes('storage automatically'));
|
||||
assert(notes.includes('sensor drivers, not a storage module'));
|
||||
assert(notes.includes('Internal flash accepts in-place deltas only'));
|
||||
assert(notes.includes('matching OTAFIX bootloader'));
|
||||
if (board === 'RAK_4631') {
|
||||
assert(notes.includes('UART2') && notes.includes('UART1') && notes.includes('W25'));
|
||||
assert(notes.includes('Legacy GPS-free Serial1') && notes.includes('Ethernet'));
|
||||
} else {
|
||||
assert(!notes.includes('UART') && !notes.includes('Ethernet'));
|
||||
}
|
||||
assert.strictEqual(JSON.stringify(profile), before, 'boardNotes must not mutate release metadata');
|
||||
for (const change of [
|
||||
{controls: undefined}, {ota: 'none'}, {sensorProfile: undefined},
|
||||
{hardware: 'RAK_4631_ethernet'}, {role: 'companion'},
|
||||
{controls: {...profile.controls, platform: 'ESP32_PLATFORM'}},
|
||||
{controls: {...profile.controls, updateRequirements: undefined}},
|
||||
{controls: {...profile.controls, updateRequirements: {lora: {storage: 'external_only'}}}},
|
||||
]) {
|
||||
assert.deepStrictEqual(picker.boardNotes({...profile, ...change}), [],
|
||||
'Unqualified adaptive-storage metadata must not create a storage promise');
|
||||
}
|
||||
}
|
||||
}
|
||||
const boardRak = boardProfile('RAK_4631_repeater_unified_lora_ota-full');
|
||||
assert(!boardText({...boardRak, controls: {...boardRak.controls, rs232: false}}).includes('UART2'));
|
||||
assert(!boardText({...boardRak, controls: {...boardRak.controls, gps: false}}).includes('UART2'));
|
||||
const boardLegacy = rakPartialCatalog.profiles.find(profile => profile.target === rakBridgeBase + '-reduced');
|
||||
assert(boardText(boardLegacy).includes('legacy GPS-free Serial1'));
|
||||
assert(!boardText(boardLegacy).includes('storage automatically'));
|
||||
assert.deepStrictEqual(picker.boardNotes({...boardLegacy,
|
||||
controls: {...boardLegacy.controls, gps: true}}), []);
|
||||
const boardEthernet = boardProfile('RAK_4631_repeater_ethernet-full');
|
||||
assert(boardText(boardEthernet).includes('separate RAK4631 Ethernet hardware image'));
|
||||
assert(!boardText(boardEthernet).includes('storage automatically'));
|
||||
assert(!boardText(boardEthernet).includes('Use UART2'));
|
||||
for (const [profile, selected] of [[tloraNormalProfile, 'UART/RS-232 choice'],
|
||||
[tloraObserverProfile, 'MQTT observer choice']]) {
|
||||
assert(boardText(profile).includes('two RAM-safe Full images'));
|
||||
assert(boardText(profile).includes(selected));
|
||||
for (const change of [{controls: undefined}, {feature: 'standard'},
|
||||
{controls: {...profile.controls, espnowBridge: false}},
|
||||
{controls: {...profile.controls, rs232: true, mqtt: true}},
|
||||
{target: profile.target.replace('V2_1_1_6', 'V2_0'), hardware: 'LilyGo_TLora_V2_0'}]) {
|
||||
assert.deepStrictEqual(picker.boardNotes({...profile, ...change}), []);
|
||||
}
|
||||
}
|
||||
assert(boardText(boardProfile('LilyGo_TLora_V2_1_1_6_repeater-full-logging'))
|
||||
.includes('UART/RS-232 choice'), 'Current UART-only logging recipe must retain the RAM warning');
|
||||
for (const profile of tripleCatalog.profiles) {
|
||||
assert(boardText(profile).includes('independent RS-232 and ESP-NOW'));
|
||||
assert.deepStrictEqual(picker.boardNotes({...profile,
|
||||
controls: {...profile.controls, rs232: false}}), []);
|
||||
}
|
||||
for (const profile of currentCatalog.profiles.filter(profile =>
|
||||
/^heltec_v4(?:_[a-z0-9]+)*_companion_radio_full/i.test(profile.target))) {
|
||||
assert(boardText(profile).includes('Full Companion is LoRa-primary'), profile.target);
|
||||
assert(!boardText(profile).includes('can bridge to ESP-NOW'));
|
||||
assert.deepStrictEqual(picker.boardNotes({...profile, controls: undefined}), []);
|
||||
assert.deepStrictEqual(picker.boardNotes({...profile,
|
||||
controls: {...profile.controls, primaryEspnow: true}}), []);
|
||||
}
|
||||
const boardV4Gateway = boardProfile('heltec_v4_repeater-full-usb-wifi');
|
||||
assert(boardText(boardV4Gateway).includes('can bridge to ESP-NOW'));
|
||||
assert.deepStrictEqual(picker.boardNotes({...boardV4Gateway,
|
||||
controls: {...boardV4Gateway.controls, espnowBridge: false}}), []);
|
||||
assert.deepStrictEqual(picker.boardNotes({...boardV4Gateway, role: 'companion'}), []);
|
||||
assert(boardText(wio).includes('flash-capacity exception'));
|
||||
assert(boardText(picker.parseTargetProfile('wio-e5_repeater')).includes('separate ordinary repeater'));
|
||||
for (const target of ['wio-e5_companion_radio_usb', 'wio-e5-mini_companion_radio_usb',
|
||||
'wio-e5-mini_repeater', 'wio-e5-dev_repeater', 'RAK_4631_companion_radio_full',
|
||||
'RAK_3401_sensor', 't1000e_companion_radio_full', 'heltec_v4_companion_radio_usb']) {
|
||||
assert.deepStrictEqual(picker.boardNotes(picker.parseTargetProfile(target)), [], target);
|
||||
}
|
||||
assert.deepStrictEqual(picker.boardNotes({...wio, controls: {platform: 'ESP32_PLATFORM'}}), []);
|
||||
console.log('exact-board notes qualify adaptive storage, UART wiring, RAM splits and gateway roles');
|
||||
|
||||
// Logging choices remain available beneath the download. Linked choices
|
||||
// preselect their exact commands; an unselected logging mode shows off steps.
|
||||
for (const logging of ['none', 'usb', 'wifi', 'both']) {
|
||||
@@ -2178,6 +2266,24 @@ global.document = {
|
||||
createTextNode: text => new RuntimeElement('#text', text),
|
||||
};
|
||||
try {
|
||||
const boardContainer = new RuntimeElement('section');
|
||||
picker.renderProfileCard(boardContainer, boardRak, boardRak.files[0], 'uf2', {}, {});
|
||||
const boardCard = boardContainer.children[0];
|
||||
const boardDetails = boardCard.children.find(node => node.className === 'firmware-picker-board-notes');
|
||||
assert(boardDetails && boardDetails.tagName === 'DETAILS');
|
||||
assert.strictEqual(!!boardDetails.open, false, 'Board notes start collapsed');
|
||||
assert.strictEqual(boardDetails.children[0].tagName, 'SUMMARY');
|
||||
assert.strictEqual(boardDetails.children[0].textContent, 'Board notes');
|
||||
assert(boardCard.children.indexOf(boardDetails) > boardCard.children.findIndex(node =>
|
||||
node.className === 'firmware-picker-actions'), 'Exact download actions stay ahead of board notes');
|
||||
assert(boardCard.children.indexOf(boardDetails) < boardCard.children.findIndex(node =>
|
||||
node.className === 'firmware-picker-steps'), 'Board notes precede installation steps');
|
||||
assert.strictEqual(boardDetails.children.filter(node => node.tagName === 'P').length,
|
||||
picker.boardNotes(boardRak).length);
|
||||
const noNotesContainer = new RuntimeElement('section');
|
||||
picker.renderProfileCard(noNotesContainer, companionEsp32, companionEsp32.files[0], 'bin', {}, {});
|
||||
assert(!noNotesContainer.children[0].children.some(node =>
|
||||
node.className === 'firmware-picker-board-notes'), 'Unaffected boards must not get an empty notes panel');
|
||||
const card = new RuntimeElement('article');
|
||||
const changes = [];
|
||||
picker.renderRuntimeDirections(card, observer, {logging: 'wifi'}, {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Offline picker packaging preserves qualified full/reduced OTA choices."""
|
||||
import importlib.util
|
||||
from html.parser import HTMLParser
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
@@ -12,7 +13,54 @@ PACKAGE = importlib.util.module_from_spec(SPEC)
|
||||
SPEC.loader.exec_module(PACKAGE)
|
||||
|
||||
|
||||
class PickerMarkup(HTMLParser):
|
||||
def __init__(self, text):
|
||||
super().__init__()
|
||||
self.tags = []
|
||||
self.feed(text)
|
||||
|
||||
def matching(self, attribute, value):
|
||||
return [(index, tag, attrs) for index, (tag, attrs) in enumerate(self.tags)
|
||||
if attrs.get(attribute) == value]
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
self.tags.append((tag, dict(attrs)))
|
||||
|
||||
|
||||
class OfflinePickerPackageTests(unittest.TestCase):
|
||||
def assert_picker_first(self, text):
|
||||
markup = PickerMarkup(text)
|
||||
form = markup.matching('data-role', 'form')
|
||||
release = markup.matching('data-role', 'release-set')
|
||||
help_panels = markup.matching('class', 'firmware-picker-help')
|
||||
self.assertEqual(len(form), 1)
|
||||
self.assertEqual(len(release), 1)
|
||||
self.assertEqual(len(help_panels), 1)
|
||||
help_index, help_tag, help_attrs = help_panels[0]
|
||||
self.assertEqual(help_tag, 'details')
|
||||
self.assertNotIn('open', help_attrs)
|
||||
self.assertLess(form[0][0], release[0][0])
|
||||
self.assertLess(markup.matching('data-role', 'result')[0][0], help_index)
|
||||
fields = [(attrs.get('data-field'), index) for index, (_, attrs)
|
||||
in enumerate(markup.tags) if 'data-field' in attrs]
|
||||
self.assertEqual(fields[0][0], 'hardwareFamily')
|
||||
field_positions = dict(fields)
|
||||
self.assertLess(field_positions['role'], field_positions['install'])
|
||||
self.assertTrue({'chipFamily', 'hardwareFamily', 'hardware', 'install',
|
||||
'role', 'logging', 'firmwareProfile', 'mode'}.issubset(
|
||||
{field for field, _ in fields}))
|
||||
for _, attrs in markup.tags:
|
||||
for described_by in attrs.get('aria-describedby', '').split():
|
||||
self.assertEqual(len(markup.matching('id', described_by)), 1)
|
||||
|
||||
def test_hosted_page_starts_with_choices_not_instructions(self):
|
||||
source = (ROOT / 'docs/firmware_picker.md').read_text(encoding='utf-8')
|
||||
title_end = source.index('# Firmware picker') + len('# Firmware picker')
|
||||
picker_start = source.index('<div class="firmware-picker"')
|
||||
self.assertFalse(source[title_end:picker_start].strip())
|
||||
self.assert_picker_first(source)
|
||||
self.assertNotIn('The unified\nRAK4631 image', source)
|
||||
|
||||
def test_sensor_policy_pairs_and_individual_downloads_are_retained(self):
|
||||
source = 'a' * 40
|
||||
family = 'v1.17.1.7-dev-aaaaaaaa'
|
||||
@@ -30,6 +78,7 @@ class OfflinePickerPackageTests(unittest.TestCase):
|
||||
uploader={'token': 'not part of the picker'}))
|
||||
releases = [dict(name=family, tag_name=family, assets=assets, authorization='private')]
|
||||
packaged = PACKAGE.package(releases, controls)
|
||||
self.assert_picker_first(packaged)
|
||||
match = re.search(r'<script type="application/json" id="firmware-picker-data">(.*?)</script>', packaged)
|
||||
embedded = json.loads(match[1])
|
||||
self.assertEqual(embedded['controls'], controls)
|
||||
|
||||
Reference in new issue
Block a user