mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-11-12 07:23:45 +08:00
Show Laboratory model pairs and selection status
This commit is contained in:
@@ -343,3 +343,31 @@ for (const label of ["Polaris R3 A ⚠️🧪", "Polaris R3 B ⚠️🧪", "Pola
|
||||
}
|
||||
assert.deepEqual(ModelsPage.methods.polarisTitleParts({value: "spz_polaris_r3a"}), ["spz_polaris_r3a", ""])
|
||||
|
||||
|
||||
// Laboratory overrides normal selections until the next model start.
|
||||
const labConfig = { enabled: true, lateralModel: 'small', longitudinalModel: 'big' }
|
||||
const labView = { ...managerPayload, laboratory: labConfig, capabilities: { ...managerPayload.capabilities, configureLaboratory: true } }
|
||||
assert.equal(modelActionAllowed(labView, 'disable-laboratory'), true)
|
||||
assert.equal(modelActionAllowed({ ...labView, isOnroad: true }, 'disable-laboratory'), false)
|
||||
assert.equal(modelActionAllowed({ ...labView, downloading: true }, 'disable-laboratory'), false)
|
||||
assert.equal(modelActionAllowed({ ...labView, laboratory: { ...labConfig, enabled: false } }, 'disable-laboratory'), false)
|
||||
const labManager = managerFixture()
|
||||
labManager.manager.start()
|
||||
await labManager.reply(0, labView)
|
||||
const disabledLab = labManager.manager.action('disable-laboratory')
|
||||
assert.equal(labManager.requests[1].url, './api/models/laboratory')
|
||||
assert.deepEqual(JSON.parse(labManager.requests[1].options.body), { ...labConfig, enabled: false })
|
||||
await labManager.reply(1, { message: 'Model Laboratory configuration saved' })
|
||||
await labManager.reply(2, { ...labView, laboratory: { ...labConfig, enabled: false } })
|
||||
await disabledLab
|
||||
assert.equal(labManager.manager.data.laboratory.enabled, false)
|
||||
labManager.manager.stop()
|
||||
const pairData = { catalog: [{ id: 'small', name: 'Small model' }, { id: 'big', name: 'Big model' }],
|
||||
laboratory: { active: true, lateralModel: 'small', longitudinalModel: 'big' } }
|
||||
const pairPage = { selectionUncertain: false, status: {}, runtime: { status: 'ready', data: pairData } }
|
||||
assert.equal(ModelsPage.computed.runningModelLabel.call(pairPage), 'Small model/Big model')
|
||||
assert.equal(ModelsPage.computed.runningModelLabel.call({ ...pairPage, runtime: { status: 'stale', data: pairData } }), 'Status unavailable')
|
||||
assert.equal(ModelsPage.computed.runningModelLabel.call({ ...pairPage, runtime: { status: 'ready', data: { ...pairData, laboratory: { ...pairData.laboratory, active: false } } } }), 'Laboratory output unavailable')
|
||||
assert.match(ModelsPage.template, /Turn off Laboratory/)
|
||||
assert.match(ModelsPage.template, /href="#\/model_laboratory"/)
|
||||
assert.match(ModelsPage.template, /fallback choices/)
|
||||
|
||||
@@ -151,10 +151,10 @@ function fileSizeText(model) {
|
||||
|
||||
const actionPaths = { "select-small": "active", "select-big": "active", favorite: "preferences", unfavorite: "preferences",
|
||||
"enable-randomizer": "preferences", "disable-randomizer": "preferences", exclude: "preferences", include: "preferences",
|
||||
download: "download", downloadAll: "download_all", cancel: "cancel", delete: "delete", refresh: "refresh_manifest", jetlink: "jetlink" }
|
||||
download: "download", downloadAll: "download_all", cancel: "cancel", delete: "delete", refresh: "refresh_manifest", jetlink: "jetlink", "disable-laboratory": "laboratory" }
|
||||
const actionCapabilities = { "select-small": "select", "select-big": "select", favorite: "favorites", unfavorite: "favorites",
|
||||
"enable-randomizer": "randomizer", "disable-randomizer": "randomizer", exclude: "exclusions", include: "exclusions",
|
||||
download: "download", downloadAll: "downloadAll", cancel: "cancel", delete: "delete", refresh: "refresh", jetlink: "jetlink" }
|
||||
download: "download", downloadAll: "downloadAll", cancel: "cancel", delete: "delete", refresh: "refresh", jetlink: "jetlink", "disable-laboratory": "configureLaboratory" }
|
||||
|
||||
export function fitsModelProfile(model, profile) {
|
||||
return Array.isArray(model?.profiles) ? model.profiles.includes(profile) : profile === "big" ? model?.requiresGpu === true : model?.requiresGpu === false
|
||||
@@ -163,6 +163,7 @@ export function fitsModelProfile(model, profile) {
|
||||
export function modelActionAllowed(data, action, model = null) {
|
||||
if (!data || data.capabilities?.[actionCapabilities[action]] !== true) return false
|
||||
if (data.isOnroad !== false && !["favorite", "unfavorite", "cancel", "select-small", "select-big"].includes(action)) return false
|
||||
if (action === "disable-laboratory") return data.laboratory?.enabled === true && !data.downloading
|
||||
if (action === "jetlink") return data.jetlink !== undefined
|
||||
if (["select-small", "select-big"].includes(action)) {
|
||||
if (action === "select-big" && data.jetlinkBig && data.isOnroad !== false) return false
|
||||
@@ -294,6 +295,7 @@ export class ModelManagerFeed {
|
||||
const data = this.data
|
||||
const key = model?.value || ""
|
||||
let payload = action === "jetlink" ? extra : {}
|
||||
if (action === "disable-laboratory") payload = { ...data.laboratory, enabled: false }
|
||||
if (action.startsWith("select-")) payload = { profile: action === "select-big" ? "big" : "small", model: key }
|
||||
else if (["favorite", "unfavorite"].includes(action)) {
|
||||
const userFavorites = data.models.filter(m => m.userFavorite && m.value !== key).map(m => m.value)
|
||||
@@ -306,7 +308,7 @@ export class ModelManagerFeed {
|
||||
} else if (["enable-randomizer", "disable-randomizer"].includes(action)) payload = { randomizer: action === "enable-randomizer" }
|
||||
else if (["download", "delete"].includes(action)) payload = { model: key }
|
||||
if (["download", "downloadAll"].includes(action)) payload.allowGpuWithoutGpu = extra.allowGpuWithoutGpu === true
|
||||
if (typeof data.view === "string") payload.view = data.view
|
||||
if (typeof data.view === "string" && action !== "disable-laboratory") payload.view = data.view
|
||||
this.saving = true
|
||||
this.data = null
|
||||
const result = await this.requestJson(actionPaths[action], payload)
|
||||
@@ -337,10 +339,15 @@ export const ModelsPage = {
|
||||
if (this.runtime.status === "unavailable" || this.runtime.status === "stale") return "Status unavailable"
|
||||
if (this.runtime.status !== "ready" || !this.runtime.data) return "Checking…"
|
||||
const model = this.runtime.data
|
||||
const pair = model.laboratory
|
||||
if (pair?.active) return [pair.lateralModel, pair.longitudinalModel].map(id => model.catalog.find(m => m.id === id)?.name || id).join("/")
|
||||
if (pair?.lateralModel) return "Laboratory output unavailable"
|
||||
if (model.health === "unavailable") return "No running model"
|
||||
if (!model.loadedId) return model.health === "loading" ? "Loading…" : "Identity unverified"
|
||||
return model.catalog.find(item => item.id === model.loadedId)?.name || model.loadedId
|
||||
},
|
||||
laboratoryEnabled() { return !this.selectionUncertain && this.status.laboratory?.enabled === true },
|
||||
laboratoryRunning() { return this.runtime.status === "ready" && !!this.runtime.data?.laboratory?.lateralModel },
|
||||
installedSmallModels() { return this.models.filter(m => (m.value === this.activeSmallModel || m.selectable) && fitsModelProfile(m, "small")) },
|
||||
installedBigModels() { return this.models.filter(m => (m.value === this.activeBigModel || m.selectable) && fitsModelProfile(m, "big")) },
|
||||
// the manager words it: under Jetlink, the Jetlink app's choice
|
||||
@@ -469,8 +476,8 @@ export const ModelsPage = {
|
||||
const result = await (["enable-randomizer", "disable-randomizer", "include", "exclude"].includes(action) ? reportSave(perform) : perform())
|
||||
if (!this.disposed && result) {
|
||||
this.trackingProgress = ["download", "downloadAll", "refresh"].includes(action)
|
||||
this.message = (this.trackingProgress && this.status.progress) || result.message ||
|
||||
(action.startsWith("select-") ? "Selection saved for the next onroad cycle." : "Saved.")
|
||||
this.message = (action === "disable-laboratory" ? "Laboratory is off for the next start. Choose your Small and Big models below." : "") || (this.trackingProgress && this.status.progress) || result.message ||
|
||||
(action === "disable-laboratory" ? "Laboratory is off for the next start. Choose your Small and Big models below." : action.startsWith("select-") ? "Selection saved for the next onroad cycle." : "Saved.")
|
||||
}
|
||||
} finally { if (!this.disposed) { this.pendingChoice = null; this.busy = "" } }
|
||||
},
|
||||
@@ -488,11 +495,25 @@ export const ModelsPage = {
|
||||
<GxNotice v-if="error" tone="danger" title="Model Manager">{{ error }}</GxNotice>
|
||||
<GxNotice v-if="status.isOnroad" tone="warn">Selections apply next onroad cycle. Park before downloading models or changing Jetlink.</GxNotice>
|
||||
<GxNotice v-if="status.downloading" :title="'Downloading ' + downloadTargetLabel">{{ status.progress || 'Keep the device connected until the download finishes.' }}</GxNotice>
|
||||
<section v-if="laboratoryEnabled || laboratoryRunning" class="gx-card">
|
||||
<div class="gx-section__header"><i aria-hidden="true" class="bi bi-bezier2"></i><span class="gx-section__title">Model Laboratory</span></div>
|
||||
<div class="gx-panel gx-stack">
|
||||
<p v-if="laboratoryEnabled">Laboratory is on. Its lateral/longitudinal pair overrides the Small/Big selections below at the next model start.</p>
|
||||
<p v-else>Laboratory is off for the next start. The currently loaded pair stays in use until then.</p>
|
||||
<p v-if="laboratoryRunning">Loaded pair: {{ runningModelLabel }}<span v-if="runtime.data.laboratory.pendingNextStart"> · saved changes pending next start</span></p>
|
||||
<p v-else-if="laboratoryEnabled">{{ runtime.status === 'ready' && runtime.data?.laboratory?.health === 'fallback' ? runtime.data.laboratory.error : 'The pair is not currently verified as running.' }}</p>
|
||||
<div class="gx-actions">
|
||||
<a class="gx-btn gx-btn--tonal" href="#/model_laboratory">Open Laboratory</a>
|
||||
<button v-if="laboratoryEnabled" type="button" class="gx-btn" :disabled="!actionAvailable('disable-laboratory')" @click="runAction('disable-laboratory')">Turn off Laboratory</button>
|
||||
</div>
|
||||
<p v-if="laboratoryEnabled && status.isOnroad" class="gx-note">Park to turn off Laboratory. Changes apply at the next model start.</p>
|
||||
</div>
|
||||
</section>
|
||||
<section class="gx-card gx-model-overview">
|
||||
<div class="gx-section__header">
|
||||
<i aria-hidden="true" class="bi bi-cpu"></i>
|
||||
<span class="gx-section__title">Models</span>
|
||||
<span v-if="currentLabel" class="gx-section__count">Selected: {{ currentLabel }}</span>
|
||||
<span v-if="currentLabel" class="gx-section__count">{{ laboratoryEnabled ? 'Normal selection:' : 'Selected:' }} {{ currentLabel }}</span>
|
||||
</div>
|
||||
<GxSummary :items="[{label: 'Installed', value: summary.installed}, {label: 'Missing', value: summary.missing}, {label: 'Total', value: summary.total}, {label: 'Running model', value: runningModelLabel}]" />
|
||||
</section>
|
||||
@@ -528,7 +549,7 @@ export const ModelsPage = {
|
||||
<span class="gx-section__title">Controls</span>
|
||||
</div>
|
||||
<div class="gx-panel gx-stack">
|
||||
<p class="gx-note">Selections take effect the next time the driving model starts.</p>
|
||||
<p class="gx-note">{{ laboratoryEnabled ? 'Small/Big selections are saved as fallback choices. Turn off Laboratory to use them normally at the next start.' : 'Selections take effect the next time the driving model starts.' }}</p>
|
||||
<div class="gx-actions">
|
||||
<button v-if="status.downloading" type="button" class="gx-btn gx-btn--danger" :disabled="!actionAvailable('cancel')" @click="runAction('cancel')"><i aria-hidden="true" class="bi bi-stop-circle"></i> Cancel Download</button>
|
||||
<button v-else type="button" class="gx-btn" :disabled="!actionAvailable('downloadAll')" @click="runAction('downloadAll')"><i aria-hidden="true" class="bi bi-download"></i> Download Missing Models</button>
|
||||
|
||||
@@ -573,6 +573,7 @@ class ModelManager:
|
||||
def snapshot(self) -> dict:
|
||||
with self.lock:
|
||||
self._refresh_if_needed()
|
||||
from openpilot.starpilot.models.laboratory import configuration
|
||||
entries, prefs = catalog(self.root), preferences(self.root)
|
||||
parked, gpu = self.parked(), self.gpu_present()
|
||||
job = self._job_status()
|
||||
@@ -599,7 +600,7 @@ class ModelManager:
|
||||
"Download required" if published else f"{GENERATION} download not published"})
|
||||
count = sum(bool(row["installed"]) for row in models)
|
||||
return {"schemaVersion": 1, "models": models, "activeSmallModel": prefs["small"], "activeBigModel": active_big,
|
||||
"jetlinkBig": jetlink_big, "bigNoneLabel": big_none,
|
||||
"jetlinkBig": jetlink_big, "bigNoneLabel": big_none, "laboratory": configuration(self.root),
|
||||
"currentModel": prefs["big"] if gpu and prefs["big"] else prefs["small"], "userFavorites": prefs["userFavorites"],
|
||||
"sortMode": prefs["sortMode"], "randomizer": prefs["randomizer"], "blacklistedModels": prefs["blacklistedModels"],
|
||||
"summary": {"total": len(models), "installed": count, "missing": len(models)-count},
|
||||
@@ -608,7 +609,8 @@ class ModelManager:
|
||||
"modelToDownload": job.get("model", ""), "downloadAll": job.get("downloadAll", False),
|
||||
"downloading": job["downloading"], "refreshing": job["refreshing"], "progress": job.get("progress", ""),
|
||||
"isOnroad": not parked, "gpuAvailable": gpu, "jetlink": jetlink,
|
||||
"capabilities": {"select": True, "download": parked, "downloadAll": parked, "cancel": True,
|
||||
"capabilities": {"configureLaboratory": parked and not job["downloading"], "select": True, "download": parked,
|
||||
"downloadAll": parked, "cancel": True,
|
||||
"jetlink": parked, "refresh": parked, "favorites": True, "delete": parked, "randomizer": parked, "exclusions": parked}}
|
||||
|
||||
def _require_parked(self) -> None:
|
||||
|
||||
@@ -89,7 +89,19 @@ class ModelStatusSource:
|
||||
"requestedId": status.requested_id, "loadedId": status.loaded_id,
|
||||
"variant": status.variant.value if status.variant is not None else None,
|
||||
"health": status.health.value, "fallbackReason": status.fallback_reason,
|
||||
"artifactSha256": status.artifact_sha256, "pendingNextStart": status.pending_next_start}
|
||||
"artifactSha256": status.artifact_sha256, "pendingNextStart": status.pending_next_start,
|
||||
"laboratory": self.laboratory_json(self.laboratory_configuration())}
|
||||
|
||||
@staticmethod
|
||||
def laboratory_configuration() -> dict:
|
||||
from openpilot.starpilot.models.laboratory import configuration
|
||||
from openpilot.starpilot.models.manager import ROOT
|
||||
return configuration(ROOT)
|
||||
|
||||
def home_label(self, commit: str = "") -> str:
|
||||
from openpilot.starpilot.ui.models_state import home_model_label
|
||||
status = self.snapshot()
|
||||
return home_model_label(status, commit, self.laboratory_json(self.laboratory_configuration()))
|
||||
|
||||
def laboratory_json(self, config: dict) -> dict:
|
||||
self.sm.update(0)
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
from dataclasses import replace
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from openpilot.starpilot.models.catalog import BUNDLED_CURRENT, model_entries
|
||||
from openpilot.starpilot.models.runtime import ModelStatusSource
|
||||
from openpilot.starpilot.models.status import ModelStatus, ModelHealth, LaboratoryLoad
|
||||
from openpilot.starpilot.models.tests.test_runtime import FakeSubMaster, FakeParams
|
||||
|
||||
|
||||
def test_actual_pair_status_reaches_home_and_galaxy_and_expires():
|
||||
sm = FakeSubMaster()
|
||||
sm.update = Mock()
|
||||
sm.messages['modelV2'].big = True
|
||||
source = ModelStatusSource(FakeParams(True), sm, lambda: 2_000_000_000)
|
||||
ids = list(model_entries())[:2]
|
||||
config = {"enabled": True, "lateralModel": ids[1], "longitudinalModel": ids[0]}
|
||||
load = LaboratoryLoad(421, 54321, 1_800_000_000, ids[0], 'v16', 'a'*64, ids[1], 'v16', 'b'*64, 'c'*64)
|
||||
normal = ModelStatus(BUNDLED_CURRENT, None, None, ModelHealth.IDENTITY_UNAVAILABLE, False, None, None)
|
||||
with patch.object(source, 'snapshot', return_value=normal), \
|
||||
patch.object(source, 'laboratory_configuration', return_value=config), \
|
||||
patch('openpilot.starpilot.models.runtime.process_start_ticks', return_value=54321), \
|
||||
patch('openpilot.starpilot.models.runtime.read_pair_receipt', return_value=load) as receipt:
|
||||
status = source.json()['laboratory']
|
||||
assert status['active'] and status['pendingNextStart']
|
||||
assert (status['lateralModel'], status['longitudinalModel']) == tuple(ids)
|
||||
assert ' / ' in source.home_label()
|
||||
sm.valid['drivingModelData'] = False
|
||||
assert source.json()['laboratory']['health'] == 'stale'
|
||||
assert source.home_label() == 'Laboratory output unavailable'
|
||||
sm.valid['drivingModelData'] = True
|
||||
receipt.return_value = replace(load, process_start_ticks=99999)
|
||||
assert not source.json()['laboratory']['active']
|
||||
assert ' / ' not in source.home_label()
|
||||
@@ -232,6 +232,14 @@ class HomeView:
|
||||
size = 30
|
||||
while size > 22 and self._measure(text, FontRole.ROMAN, size).width > width:
|
||||
size -= 1
|
||||
if y == 140 and " / " in text:
|
||||
parts = text.split(" / ", 1)
|
||||
budget = (width - self._measure(" / ", FontRole.ROMAN, size).width) / 2
|
||||
for i, part in enumerate(parts):
|
||||
while part and self._measure(part, FontRole.ROMAN, size).width > budget:
|
||||
part = part[:-2].rstrip('…') + '…'
|
||||
parts[i] = part
|
||||
text = " / ".join(parts)
|
||||
while text and self._measure(text, FontRole.ROMAN, size).width > width:
|
||||
text = text[:-2].rstrip('…') + '…'
|
||||
self._text(text, FontRole.ROMAN, size, 6, y, color)
|
||||
|
||||
@@ -136,7 +136,17 @@ def model_display_text(value: str) -> str:
|
||||
unicodedata.category(char) != "So" and char not in "\ufe0e\ufe0f\u200d").split())
|
||||
|
||||
|
||||
def home_model_label(status: ModelStatus, commit: str = "") -> str:
|
||||
def home_model_label(status: ModelStatus, commit: str = "", laboratory: dict | None = None) -> str:
|
||||
if laboratory:
|
||||
if laboratory.get("active") is True:
|
||||
entries = model_entries()
|
||||
names = [entries[mid].name if mid in entries else mid
|
||||
for mid in (laboratory["lateralModel"], laboratory["longitudinalModel"])]
|
||||
return model_display_text(" / ".join(names))
|
||||
if laboratory.get("lateralModel"):
|
||||
return "Laboratory output unavailable"
|
||||
if laboratory.get("requested") and laboratory.get("health") != "fallback":
|
||||
return "Model Laboratory · next start" if laboratory.get("health") == "unavailable" else "Model Laboratory · starting"
|
||||
entry = model_entries().get(status.loaded_id or status.requested_id)
|
||||
if entry is None:
|
||||
return "Driving model unavailable"
|
||||
|
||||
@@ -57,7 +57,7 @@ from openpilot.starpilot.ui.feature_settings_state import (
|
||||
from openpilot.starpilot.ui.lane_change_feature import KEYS as LANE_CHANGE_KEYS, RESET as LANE_CHANGE_RESET
|
||||
from openpilot.starpilot.models.runtime import ModelStatusSource
|
||||
from openpilot.starpilot.models.status import project_status
|
||||
from openpilot.starpilot.ui.models_state import model_page, model_profile_choices, model_profile_request, home_model_label
|
||||
from openpilot.starpilot.ui.models_state import model_page, model_profile_choices, model_profile_request
|
||||
from openpilot.starpilot.ui.maps_state import MapStatusSource
|
||||
from openpilot.starpilot.galaxy.access import GalaxyAccessOwner, default_owner
|
||||
from openpilot.starpilot.ui.galaxy_access import GalaxyAccessFlow
|
||||
@@ -1319,7 +1319,7 @@ class StarShellSession:
|
||||
try:
|
||||
if self.model_source is None:
|
||||
self.model_source = ModelStatusSource(self.adapter.ui_state.params)
|
||||
label = home_model_label(self.model_source.snapshot(), snapshot.home.commit if self.profile == Profile.COMPACT else "")
|
||||
label = self.model_source.home_label(snapshot.home.commit if self.profile == Profile.COMPACT else "")
|
||||
except (OSError, RuntimeError, ValueError):
|
||||
label = "Driving model unavailable"
|
||||
self._home_model = (now, label)
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import Mock
|
||||
|
||||
from openpilot.starpilot.models.catalog import model_entries, BUNDLED_CURRENT
|
||||
from openpilot.starpilot.models.status import ModelStatus, ModelHealth
|
||||
from openpilot.starpilot.ui.home import HomeView
|
||||
from openpilot.starpilot.ui.models_state import home_model_label, model_display_text
|
||||
|
||||
|
||||
def test_home_pair_uses_verified_roles_and_never_saved_choices():
|
||||
ids = list(model_entries())[:2]
|
||||
normal = ModelStatus(BUNDLED_CURRENT, None, None, ModelHealth.UNAVAILABLE, False, None, None)
|
||||
pair = {"active": True, "lateralModel": ids[0], "longitudinalModel": ids[1], "requested": False, "pendingNextStart": True}
|
||||
names = [model_display_text(model_entries()[mid].name) for mid in ids]
|
||||
assert home_model_label(normal, "abcdef123", pair) == " / ".join(names)
|
||||
assert home_model_label(normal, laboratory=pair | {"active": False}) == "Laboratory output unavailable"
|
||||
assert home_model_label(normal, laboratory={"requested": True, "health": "unavailable"}) == "Model Laboratory · next start"
|
||||
assert home_model_label(normal, laboratory={"requested": True, "health": "fallback"}) == home_model_label(normal)
|
||||
|
||||
|
||||
def test_compact_keeps_both_names_inside_the_model_row():
|
||||
view = HomeView.__new__(HomeView)
|
||||
view._text = Mock()
|
||||
view._icon = Mock()
|
||||
view._measure = lambda text, role, size: SimpleNamespace(width=len(text) * size * .6)
|
||||
state = SimpleNamespace(version="7", branch="Dom", commit_date="", model_label="A" * 60 + " / " + "B" * 60,
|
||||
network="none", network_strength=0)
|
||||
# Stop after rendering the header; later sections need unrelated home state.
|
||||
class EndHeader(Exception):
|
||||
pass
|
||||
view._icon.side_effect = EndHeader
|
||||
try:
|
||||
view._compact(state)
|
||||
except EndHeader:
|
||||
pass
|
||||
drawn = next(call.args for call in view._text.call_args_list if len(call.args) > 4 and call.args[4] == 140)
|
||||
text, role, size, x, y, color = drawn
|
||||
assert text.startswith("A") and " / B" in text
|
||||
assert "Lateral" not in text and "Longitudinal" not in text
|
||||
assert view._measure(text, role, size).width <= 520
|
||||
+1
-1
@@ -1 +1 @@
|
||||
{"protected_inventory_sha256": "1afefe80e3fd4ee15afd2a8b1c1108543dfd0547089bb508caf646d6c2a8b40b", "schema_version": 1}
|
||||
{"protected_inventory_sha256": "26ffbe8157fb1f59f9cdf5141380ff040e251450435acd02ac0dfe08c5bffe4f", "schema_version": 1}
|
||||
|
||||
Reference in New Issue
Block a user