guild charters: the registrar, the charter, and the twelve verbs
- founding a guild: fourteen opcodes, eight goldens (four mutation-checked), twelve engine globals, `GuildRegistrarFrame` and `PetitionFrame` — `CMSG_PETITION_BUY` is a 72-byte frame the server reads two fields of and drops in silence if short, `allowedGender` is the family's one u16, a signature is a 12-byte stride not 8, and `MSG_PETITION_DECLINE` writes the charter guid and reads the decliner's — same width, opposite meaning
- two sessions, not one: the registrar is an NPC session (closed by walking away — its show event is one of the fourteen callers of the interact latch, so the registrar turns to face you) and the charter is bound to an item guid you carry across the city; right-clicking a charter in your bags opens it, keyed on template flag `0x2000` (item 5863 has no on-use spell and no quest, so every existing item-use arm declined it)
- the byte carve came back and rescored six named guesses — three confirmed, two corrected, one refuted: `PETITION_SHOW` is deferred until every signer name has resolved (the window never paints blank rows), `ClosePetition` on somebody else's charter sends `MSG_PETITION_DECLINE` ("sends nothing" had been chosen on principle and was wrong half the time), and founding a guild is silent — no `ERR_GUILD_FOUNDER_S`, the reference emits no text at all; the four success ids go to chat and the five refusals to the red error frame, three paraphrased strings are now the shipped ones (a sweep asserts all fifteen), and the `npc` unit token had never been fed — the registrar's name banner was blank; `WOW_PROBE_CHARTER=1` drives gossip → buy → open → rename → destroy live, 10/10
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -923,6 +923,12 @@ mod guild_tests;
|
||||
#[cfg(test)]
|
||||
mod help_frame_tests;
|
||||
|
||||
/// The two guild-charter windows — the registrar and the petition sheet (decision 1672). Its own
|
||||
/// module for `guild_tests`' reason: it stands the charter engine API in for in Lua before the XML
|
||||
/// loads, so what is under test is the window and not `script::petition`'s plumbing.
|
||||
#[cfg(test)]
|
||||
mod petition_tests;
|
||||
|
||||
/// The social window's fourth tab — the raid pane and its grid (decision 1549). Its own module for
|
||||
/// `guild_tests`' reason: every test in it pushes a RAID roster first, which a file about the
|
||||
/// friends list must not be in the business of.
|
||||
|
||||
@@ -0,0 +1,454 @@
|
||||
//! The two guild-charter windows (decision 1672): the guild registrar's two panels, and the
|
||||
//! charter itself with its two faces.
|
||||
//!
|
||||
//! What these guard that the Rust-side unit tests structurally cannot: the windows are Lua over an
|
||||
//! engine snapshot, so a `GetPetitionInfo` destructured in the wrong order, a name row wired to the
|
||||
//! wrong index, the leader/signer swap tested the wrong way round, or a button pointed at the wrong
|
||||
//! verb are all invisible to `script::petition`'s own tests and green in the parse sweep. Each test
|
||||
//! below fails on exactly one of those.
|
||||
//!
|
||||
//! **The engine API is STOOD IN FOR here, deliberately** — `guild_tests`' convention, and for its
|
||||
//! reasons: what is under test is the window and nothing else, the tests need no app feed seated,
|
||||
//! and a change to the engine's plumbing cannot quietly turn one green. Two fixture shapes are the
|
||||
//! ones `script::petition` promises and are easy to get wrong:
|
||||
//!
|
||||
//! - **era booleans are `1`/`nil`, never `true`/`false`** — `isOriginator` and `CanSignPetition`;
|
||||
//! - **`GetPetitionInfo` returns SIX values in a fixed order**
|
||||
//! (`petitionType, title, bodyText, maxSignatures, originatorName, isOriginator`), and the
|
||||
//! fourth is the wire's *requirement*, not `MAX_PETITION_SIGNATURES`.
|
||||
|
||||
use benilla_ui::script::UiScript;
|
||||
|
||||
/// Load one shipped `assets/ui/<file>`, panicking on any loader error **and on any
|
||||
/// unknown-template warning** — `guild_tests`' loader, and the decision-0672 reason is the same:
|
||||
/// `inherits=` a template this house doesn't ship is a *warning*, the frame still loads, and a
|
||||
/// bare Button still clicks. Both windows here inherit from `UIPanelTemplates.xml` and
|
||||
/// `MoneyFrame.xml`, and the registrar defines one local template of its own.
|
||||
fn load_xml(s: &UiScript, file: &str) {
|
||||
let text = std::fs::read_to_string(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("assets/ui")
|
||||
.join(file),
|
||||
)
|
||||
.unwrap();
|
||||
let doc = benilla_ui::framexml::parse(&text).unwrap();
|
||||
let report = benilla_ui::loader::load(s, &doc, &|_| None);
|
||||
assert!(
|
||||
report.errors.is_empty(),
|
||||
"{file}: loader errors: {:?}",
|
||||
report.errors
|
||||
);
|
||||
let missing: Vec<&String> = report
|
||||
.warnings
|
||||
.iter()
|
||||
.filter(|w| w.contains("unknown template"))
|
||||
.collect();
|
||||
assert!(
|
||||
missing.is_empty(),
|
||||
"{file}: inherits a template this house does not ship (the frame loads, its ART does \
|
||||
not): {missing:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// The charter engine API, stood in for in Lua.
|
||||
///
|
||||
/// One mutable table, `BenillaPetitionFixture`, is the whole model; every verb appends to `.calls`,
|
||||
/// which a test drains with `BenillaPetitionCalls()`. Tests mutate the table's FIELDS — never
|
||||
/// replace the table, since the closures below hold it as an upvalue.
|
||||
///
|
||||
/// Seeded as a **signer's** view of a charter with two of nine signatures: the case where every
|
||||
/// distinction this window makes is live at once (the Sign face showing, rows both filled and
|
||||
/// empty, Request still enabled).
|
||||
const PETITION_FIXTURE: &str = r#"
|
||||
BenillaPetitionFixture = {
|
||||
petitionType = "charter",
|
||||
title = "Legacy of Steel",
|
||||
bodyText = "",
|
||||
-- The WIRE's requirement, deliberately not 9, so a test can tell it apart from
|
||||
-- MAX_PETITION_SIGNATURES.
|
||||
maxSignatures = 4,
|
||||
originator = "Tigole",
|
||||
isOriginator = nil,
|
||||
canSign = 1,
|
||||
signers = { "Furor", "Kaplan" },
|
||||
charterCost = 1000,
|
||||
calls = {},
|
||||
}
|
||||
|
||||
local F = BenillaPetitionFixture
|
||||
|
||||
function BenillaPetitionCalls()
|
||||
local out = table.concat(F.calls, "|")
|
||||
F.calls = {}
|
||||
return out
|
||||
end
|
||||
|
||||
local function record(call)
|
||||
table.insert(F.calls, call)
|
||||
end
|
||||
|
||||
function GetPetitionInfo()
|
||||
return F.petitionType, F.title, F.bodyText, F.maxSignatures, F.originator, F.isOriginator
|
||||
end
|
||||
|
||||
function GetNumPetitionNames() return table.getn(F.signers) end
|
||||
function GetPetitionNameInfo(i) return F.signers[i] end
|
||||
function CanSignPetition() return F.canSign end
|
||||
function GetGuildCharterCost() return F.charterCost end
|
||||
|
||||
function SignPetition() record("SignPetition") end
|
||||
function OfferPetition() record("OfferPetition") end
|
||||
function ClosePetition() record("ClosePetition") end
|
||||
function CloseGuildRegistrar() record("CloseGuildRegistrar") end
|
||||
function TurnInGuildCharter() record("TurnInGuildCharter") end
|
||||
function BuyGuildCharter(name) record("BuyGuildCharter:" .. name) end
|
||||
function RenamePetition(name) record("RenamePetition:" .. name) end
|
||||
"#;
|
||||
|
||||
/// The windows' manifest slice, in `benilla.toc` order, with the fixture seated first.
|
||||
///
|
||||
/// `MoneyFrame.xml` is load-bearing rather than incidental: `GuildRegistrarMoneyFrame` declares
|
||||
/// `inherits="MoneyFrameTemplate"`, which resolves at LOAD (decision 1580), so without it the
|
||||
/// registrar's price row would be a bare frame and the unknown-template guard above would fire.
|
||||
fn setup() -> UiScript {
|
||||
let mut s = UiScript::new().unwrap();
|
||||
s.set_screen_size(1024.0, 768.0);
|
||||
s.run(PETITION_FIXTURE).unwrap();
|
||||
load_xml(&s, "Fonts.xml");
|
||||
load_xml(&s, "BasicControls.xml");
|
||||
load_xml(&s, "MoneyFrame.xml");
|
||||
load_xml(&s, "UiPanels.xml");
|
||||
load_xml(&s, "UIPanelTemplates.xml");
|
||||
load_xml(&s, "GuildRegistrarFrame.xml");
|
||||
load_xml(&s, "PetitionFrame.xml");
|
||||
s
|
||||
}
|
||||
|
||||
fn text(s: &UiScript, expr: &str) -> String {
|
||||
s.eval::<String>(&format!("return {expr}:GetText() or \"\""))
|
||||
.unwrap_or_else(|e| panic!("{expr}:GetText() — {e}"))
|
||||
}
|
||||
|
||||
fn visible(s: &UiScript, frame: &str) -> bool {
|
||||
s.eval::<bool>(&format!("return {frame}:IsVisible()"))
|
||||
.unwrap_or_else(|e| panic!("{frame}:IsVisible() — {e}"))
|
||||
}
|
||||
|
||||
fn calls(s: &UiScript) -> String {
|
||||
s.eval::<String>("return BenillaPetitionCalls()").unwrap()
|
||||
}
|
||||
|
||||
/// Open the charter window the way the engine does.
|
||||
fn show_petition(s: &mut UiScript) {
|
||||
s.fire_event("PETITION_SHOW", vec![]);
|
||||
assert!(s.errors().is_empty(), "PETITION_SHOW: {:?}", s.errors());
|
||||
}
|
||||
|
||||
fn show_registrar(s: &mut UiScript) {
|
||||
s.fire_event("GUILD_REGISTRAR_SHOW", vec![]);
|
||||
assert!(
|
||||
s.errors().is_empty(),
|
||||
"GUILD_REGISTRAR_SHOW: {:?}",
|
||||
s.errors()
|
||||
);
|
||||
}
|
||||
|
||||
/// A signer's charter paints the header, the signed rows, the unsigned rows, and the member
|
||||
/// instructions — and shows Sign, not Request/Rename.
|
||||
///
|
||||
/// The row assertion is the load-bearing half: rows 1..2 carry the two signers and rows 3..9 must
|
||||
/// read `<not yet signed>`. A `GetPetitionNameInfo` bound 0-based would put "Kaplan" in row 1 and
|
||||
/// blank row 2 while every other assertion here still passed.
|
||||
#[test]
|
||||
fn a_signers_charter_shows_the_sign_face_and_nine_rows() {
|
||||
let mut s = setup();
|
||||
show_petition(&mut s);
|
||||
|
||||
assert!(visible(&s, "PetitionFrame"));
|
||||
assert_eq!(
|
||||
text(&s, "PetitionFrameNpcNameText"),
|
||||
"Legacy of Steel Guild Charter",
|
||||
"GUILD_CHARTER_TEMPLATE filled with the title"
|
||||
);
|
||||
assert_eq!(text(&s, "PetitionFrameCharterName"), "Legacy of Steel");
|
||||
assert_eq!(text(&s, "PetitionFrameMasterName"), "Tigole");
|
||||
// The three static labels come from GlobalStrings via `text=` and are never painted.
|
||||
assert_eq!(text(&s, "PetitionFrameCharterTitle"), "Guild Name");
|
||||
assert_eq!(text(&s, "PetitionFrameMasterTitle"), "Guild Master");
|
||||
assert_eq!(text(&s, "PetitionFrameMemberTitle"), "Members");
|
||||
|
||||
assert_eq!(text(&s, "PetitionFrameMemberName1"), "Furor");
|
||||
assert_eq!(text(&s, "PetitionFrameMemberName2"), "Kaplan");
|
||||
for i in 3..=9 {
|
||||
assert_eq!(
|
||||
text(&s, &format!("PetitionFrameMemberName{i}")),
|
||||
"<not yet signed>",
|
||||
"row {i} is an empty seat"
|
||||
);
|
||||
}
|
||||
|
||||
assert!(visible(&s, "PetitionFrameSignButton"));
|
||||
assert!(!visible(&s, "PetitionFrameRequestButton"));
|
||||
assert!(
|
||||
!visible(&s, "PetitionFrameRenameButton"),
|
||||
"only the charter's owner may rename it"
|
||||
);
|
||||
assert_eq!(
|
||||
text(&s, "PetitionFrameInstructions"),
|
||||
"Click the <Sign Charter> button to become a charter member of this guild."
|
||||
);
|
||||
}
|
||||
|
||||
/// The owner's charter is the same window inside out: Request + Rename replace Sign, and the
|
||||
/// instructions change.
|
||||
///
|
||||
/// Both halves are asserted because the two buttons share one anchor — a Show/Hide written the
|
||||
/// wrong way round stacks them and the top one wins, which looks correct from a screenshot of
|
||||
/// either view alone.
|
||||
#[test]
|
||||
fn the_owners_charter_swaps_sign_for_request_and_rename() {
|
||||
let mut s = setup();
|
||||
s.run("BenillaPetitionFixture.isOriginator = 1").unwrap();
|
||||
show_petition(&mut s);
|
||||
|
||||
assert!(visible(&s, "PetitionFrameRequestButton"));
|
||||
assert!(visible(&s, "PetitionFrameRenameButton"));
|
||||
assert!(!visible(&s, "PetitionFrameSignButton"));
|
||||
assert_eq!(
|
||||
text(&s, "PetitionFrameInstructions"),
|
||||
"Select a player you wish to invite and click <request signature>. To create this \
|
||||
guild, turn it in to the guild registrar when you have filled the charter."
|
||||
);
|
||||
}
|
||||
|
||||
/// Request Signature disables against the **wire's** requirement, never against the nine rows.
|
||||
///
|
||||
/// The fixture requires 4. At three signatures the button is live; at four it is not — and nine
|
||||
/// rows are still painted either way. A window that compared against `MAX_PETITION_SIGNATURES`
|
||||
/// would leave Request enabled on a full charter, and every other assertion in this file would
|
||||
/// still pass.
|
||||
#[test]
|
||||
fn request_signature_disables_at_the_wires_requirement_not_at_nine() {
|
||||
let mut s = setup();
|
||||
s.run(
|
||||
r#"
|
||||
BenillaPetitionFixture.isOriginator = 1
|
||||
BenillaPetitionFixture.signers = { "A", "B", "C" }
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
show_petition(&mut s);
|
||||
assert!(
|
||||
s.eval::<bool>("return PetitionFrameRequestButton:IsEnabled() and true or false")
|
||||
.unwrap(),
|
||||
"three of four signatures — still asking"
|
||||
);
|
||||
|
||||
s.run(r#"BenillaPetitionFixture.signers = { "A", "B", "C", "D" }"#)
|
||||
.unwrap();
|
||||
show_petition(&mut s);
|
||||
assert!(
|
||||
!s.eval::<bool>("return PetitionFrameRequestButton:IsEnabled() and true or false")
|
||||
.unwrap(),
|
||||
"four of four — the charter is full, though only four of nine rows are used"
|
||||
);
|
||||
assert_eq!(
|
||||
text(&s, "PetitionFrameMemberName5"),
|
||||
"<not yet signed>",
|
||||
"the nine rows are unaffected by the requirement"
|
||||
);
|
||||
}
|
||||
|
||||
/// `CanSignPetition` gates the Sign button independently of which face is showing: a charter can be
|
||||
/// a signer's view and still be unsignable (already signed, already guilded, full).
|
||||
#[test]
|
||||
fn the_sign_button_follows_can_sign_petition() {
|
||||
let mut s = setup();
|
||||
show_petition(&mut s);
|
||||
assert!(s
|
||||
.eval::<bool>("return PetitionFrameSignButton:IsEnabled() and true or false")
|
||||
.unwrap());
|
||||
|
||||
s.run("BenillaPetitionFixture.canSign = nil").unwrap();
|
||||
show_petition(&mut s);
|
||||
assert!(
|
||||
!s.eval::<bool>("return PetitionFrameSignButton:IsEnabled() and true or false")
|
||||
.unwrap(),
|
||||
"nil, the era false, disables it"
|
||||
);
|
||||
assert!(
|
||||
visible(&s, "PetitionFrameSignButton"),
|
||||
"disabled, not hidden — hiding would expose the Request button beneath it"
|
||||
);
|
||||
}
|
||||
|
||||
/// Each button reaches its own verb, and closing the window closes the engine's session.
|
||||
///
|
||||
/// The `ClosePetition` half is the one worth pinning: it rides `OnHide`, so it fires however the
|
||||
/// window closes, and without it the engine keeps a charter session for a window nobody can see.
|
||||
#[test]
|
||||
fn the_charter_buttons_reach_their_verbs_and_the_close_clears_the_session() {
|
||||
let mut s = setup();
|
||||
show_petition(&mut s);
|
||||
let _ = calls(&s);
|
||||
|
||||
s.run("PetitionFrameSignButton:Click()").unwrap();
|
||||
assert_eq!(calls(&s), "SignPetition");
|
||||
|
||||
s.run("BenillaPetitionFixture.isOriginator = 1").unwrap();
|
||||
show_petition(&mut s);
|
||||
let _ = calls(&s);
|
||||
s.run("PetitionFrameRequestButton:Click()").unwrap();
|
||||
assert_eq!(calls(&s), "OfferPetition");
|
||||
|
||||
s.run("HideUIPanel(PetitionFrame)").unwrap();
|
||||
assert!(!visible(&s, "PetitionFrame"));
|
||||
assert_eq!(calls(&s), "ClosePetition", "OnHide clears the session");
|
||||
}
|
||||
|
||||
/// The rename dialog is registered, takes the box's text, and is capped at the server's own
|
||||
/// 24-character charter-name limit.
|
||||
#[test]
|
||||
fn rename_guild_sends_the_box_text_and_caps_at_twenty_four() {
|
||||
let mut s = setup();
|
||||
s.run("BenillaPetitionFixture.isOriginator = 1").unwrap();
|
||||
show_petition(&mut s);
|
||||
let _ = calls(&s);
|
||||
|
||||
s.run("PetitionFrameRenameButton:Click()").unwrap();
|
||||
assert!(s.errors().is_empty(), "{:?}", s.errors());
|
||||
assert_eq!(
|
||||
s.eval::<i64>("return StaticPopupDialogs[\"RENAME_GUILD\"].maxLetters")
|
||||
.unwrap(),
|
||||
24,
|
||||
"the server's MAX_CHARTER_NAME"
|
||||
);
|
||||
assert!(
|
||||
visible(&s, "StaticPopup1"),
|
||||
"the popup engine raised the dialog"
|
||||
);
|
||||
s.run(
|
||||
r#"
|
||||
StaticPopup1EditBox:SetText("Second Legacy")
|
||||
StaticPopup1Button1:Click()
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(calls(&s), "RenamePetition:Second Legacy");
|
||||
}
|
||||
|
||||
/// The registrar opens on its services list, and Purchase swaps panels **locally** — no verb, no
|
||||
/// packet. Getting that wrong would send a buy on the first click of "Purchase a Guild Charter",
|
||||
/// before the player has typed a name.
|
||||
#[test]
|
||||
fn the_registrar_opens_on_services_and_purchase_is_a_local_panel_swap() {
|
||||
let mut s = setup();
|
||||
show_registrar(&mut s);
|
||||
|
||||
assert!(visible(&s, "GuildRegistrarFrame"));
|
||||
assert!(visible(&s, "GuildRegistrarGreetingFrame"));
|
||||
assert!(!visible(&s, "GuildRegistrarPurchaseFrame"));
|
||||
assert_eq!(
|
||||
text(&s, "GuildRegistrarButton1"),
|
||||
"Purchase a Guild Charter"
|
||||
);
|
||||
assert_eq!(
|
||||
text(&s, "GuildRegistrarButton2"),
|
||||
"Register a Guild Charter"
|
||||
);
|
||||
let _ = calls(&s);
|
||||
|
||||
s.run("GuildRegistrarButton1:Click()").unwrap();
|
||||
assert!(visible(&s, "GuildRegistrarPurchaseFrame"));
|
||||
assert!(!visible(&s, "GuildRegistrarGreetingFrame"));
|
||||
assert_eq!(calls(&s), "", "the swap sends nothing");
|
||||
}
|
||||
|
||||
/// Buying sends the box's text and closes the window; registering sends the no-argument turn-in.
|
||||
///
|
||||
/// The close-after-buy is the reference's own behaviour and is asserted because it looks like a bug
|
||||
/// otherwise: a buy is one-shot, and its answer is an item arriving rather than anything this
|
||||
/// window will be told.
|
||||
#[test]
|
||||
fn purchase_sends_the_typed_name_and_register_turns_the_charter_in() {
|
||||
let mut s = setup();
|
||||
show_registrar(&mut s);
|
||||
s.run("GuildRegistrarButton1:Click()").unwrap();
|
||||
let _ = calls(&s);
|
||||
|
||||
s.run(
|
||||
r#"
|
||||
GuildRegistrarFrameEditBox:SetText("Legacy of Steel")
|
||||
GuildRegistrarFramePurchaseButton:Click()
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
calls(&s),
|
||||
"BuyGuildCharter:Legacy of Steel|CloseGuildRegistrar",
|
||||
"the buy, then the window's own OnHide close"
|
||||
);
|
||||
assert!(!visible(&s, "GuildRegistrarFrame"));
|
||||
|
||||
show_registrar(&mut s);
|
||||
let _ = calls(&s);
|
||||
s.run("GuildRegistrarButton2:Click()").unwrap();
|
||||
assert_eq!(
|
||||
calls(&s),
|
||||
"TurnInGuildCharter",
|
||||
"no argument — the engine finds the charter in the bags"
|
||||
);
|
||||
}
|
||||
|
||||
/// The price row is filled at panel-swap time from `GetGuildCharterCost()`, in copper.
|
||||
///
|
||||
/// Reading it at *show* time instead would paint a 0: the cost belongs to the open registrar's
|
||||
/// charter list, and the greeting panel never displays one.
|
||||
#[test]
|
||||
fn the_charter_price_is_read_when_the_purchase_panel_opens() {
|
||||
let mut s = setup();
|
||||
show_registrar(&mut s);
|
||||
s.run("GuildRegistrarButton1:Click()").unwrap();
|
||||
// 1000 copper = 10 silver: the gold slot is empty, the silver slot reads 10.
|
||||
assert_eq!(text(&s, "GuildRegistrarMoneyFrameSilverButtonText"), "10");
|
||||
assert_eq!(text(&s, "GuildRegistrarCostLabel"), "Cost:");
|
||||
}
|
||||
|
||||
/// Re-opening the registrar always lands on the services list, never on a half-filled purchase
|
||||
/// panel left over from last time.
|
||||
#[test]
|
||||
fn reopening_the_registrar_returns_to_the_services_list() {
|
||||
let mut s = setup();
|
||||
show_registrar(&mut s);
|
||||
s.run("GuildRegistrarButton1:Click()").unwrap();
|
||||
assert!(visible(&s, "GuildRegistrarPurchaseFrame"));
|
||||
|
||||
s.fire_event("GUILD_REGISTRAR_CLOSED", vec![]);
|
||||
assert!(!visible(&s, "GuildRegistrarFrame"));
|
||||
show_registrar(&mut s);
|
||||
assert!(visible(&s, "GuildRegistrarGreetingFrame"));
|
||||
assert!(!visible(&s, "GuildRegistrarPurchaseFrame"));
|
||||
}
|
||||
|
||||
/// Both windows are registered UIPanels — without a row, `ShowUIPanel` degrades to a bare `Show()`
|
||||
/// in no slot and two left-slot windows paint over each other (B288's shape, decision 1507).
|
||||
#[test]
|
||||
fn both_charter_windows_are_registered_left_slot_panels() {
|
||||
let mut s = setup();
|
||||
for frame in ["GuildRegistrarFrame", "PetitionFrame"] {
|
||||
assert_eq!(
|
||||
s.eval::<String>(&format!("return UIPanelWindows[\"{frame}\"].area"))
|
||||
.unwrap(),
|
||||
"left",
|
||||
"{frame} must hold a panel row"
|
||||
);
|
||||
}
|
||||
// And they are rivals, not neighbours: opening one seats it where the other was.
|
||||
show_registrar(&mut s);
|
||||
show_petition(&mut s);
|
||||
assert!(visible(&s, "PetitionFrame"));
|
||||
assert!(
|
||||
!visible(&s, "GuildRegistrarFrame"),
|
||||
"one left slot, both pushable 0"
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user