mirror of
https://github.com/Kelsidavis/WoWee.git
synced 2026-08-04 11:55:07 -04:00
A frame written at the top level with parent="Something" belongs to that frame, and its $parent means it. WorldMapTitleButton is declared that way and anchors to $parentMiniBorderLeft — the world map's own border. Resolved against where it sits in the file instead, there was no containing frame to take a name from, so $parent collapsed to the bare suffix and SetPoint looked up a global called MiniBorderLeft that nothing has. The anchor then fell back to the parent and the title bar went wherever that put it. 231 frames across FrameXML declare a parent this way. Found by reading the missing-API list from a real session rather than by guessing: bare MiniBorderLeft in that list is not a missing API at all, it is a name this emitter built wrong.
642 lines
29 KiB
C++
642 lines
29 KiB
C++
#include <catch_amalgamated.hpp>
|
|
|
|
#include "ui/xml_parser.hpp"
|
|
#include "ui/framexml_emitter.hpp"
|
|
|
|
#include <string>
|
|
|
|
using namespace wowee::ui;
|
|
|
|
namespace {
|
|
bool has(const std::string& haystack, const std::string& needle) {
|
|
return haystack.find(needle) != std::string::npos;
|
|
}
|
|
XmlNode parseOrFail(const std::string& src) {
|
|
XmlNode root;
|
|
std::string err;
|
|
REQUIRE(parseXml(src, root, err));
|
|
INFO(err);
|
|
return root;
|
|
}
|
|
}
|
|
|
|
// ── Parser ──────────────────────────────────────────────────────────────────
|
|
|
|
TEST_CASE("Attributes, nesting and self-closing elements", "[framexml][xml]") {
|
|
XmlNode root = parseOrFail(
|
|
"<Ui><Frame name='A' hidden='true'><Size><AbsDimension x='10' y='20'/></Size></Frame></Ui>");
|
|
REQUIRE(root.name == "Ui");
|
|
REQUIRE(root.children.size() == 1);
|
|
const XmlNode& f = root.children[0];
|
|
REQUIRE(f.name == "Frame");
|
|
REQUIRE(f.attrOr("name", "") == "A");
|
|
REQUIRE(f.attrBool("hidden"));
|
|
const XmlNode* dim = f.child("Size")->child("AbsDimension");
|
|
REQUIRE(dim != nullptr);
|
|
REQUIRE(dim->attrFloat("x") == Catch::Approx(10.0f));
|
|
REQUIRE(dim->attrFloat("y") == Catch::Approx(20.0f));
|
|
}
|
|
|
|
TEST_CASE("The declaration and comments are skipped", "[framexml][xml]") {
|
|
XmlNode root = parseOrFail(
|
|
"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
|
|
"<!-- a comment, possibly with <tags> inside -->\n"
|
|
"<Ui><!-- another --><Frame name=\"A\"/></Ui>");
|
|
REQUIRE(root.name == "Ui");
|
|
REQUIRE(root.children.size() == 1);
|
|
REQUIRE(root.children[0].attrOr("name", "") == "A");
|
|
}
|
|
|
|
TEST_CASE("CDATA is taken verbatim", "[framexml][xml]") {
|
|
// The reason CDATA matters: this is Lua, and decoding entities inside it
|
|
// would turn every comparison into something that will not compile.
|
|
XmlNode root = parseOrFail(
|
|
"<Ui><Frame name=\"A\"><Scripts><OnLoad><![CDATA[\n"
|
|
"if a < b and c > d then self:Show() end\n"
|
|
"]]></OnLoad></Scripts></Frame></Ui>");
|
|
const XmlNode* onLoad = root.children[0].child("Scripts")->child("OnLoad");
|
|
REQUIRE(onLoad != nullptr);
|
|
REQUIRE(has(onLoad->text, "a < b and c > d"));
|
|
}
|
|
|
|
TEST_CASE("Entities decode outside CDATA", "[framexml][xml]") {
|
|
XmlNode root = parseOrFail("<Ui><Frame text=\"Fish & Chips <3\"/></Ui>");
|
|
REQUIRE(root.children[0].attrOr("text", "") == "Fish & Chips <3");
|
|
}
|
|
|
|
TEST_CASE("Malformed input is reported, not thrown", "[framexml][xml]") {
|
|
// One bad file among a hundred must not take the rest of the interface down.
|
|
XmlNode root;
|
|
std::string err;
|
|
REQUIRE_FALSE(parseXml("<Ui><Frame></Ui>", root, err));
|
|
REQUIRE_FALSE(err.empty());
|
|
|
|
REQUIRE_FALSE(parseXml("", root, err));
|
|
REQUIRE_FALSE(parseXml("<Ui><Frame name=unquoted/></Ui>", root, err));
|
|
}
|
|
|
|
// ── Emitter ─────────────────────────────────────────────────────────────────
|
|
|
|
TEST_CASE("A frame emits CreateFrame with its type and parent", "[framexml][emit]") {
|
|
XmlNode root = parseOrFail("<Ui><Button name=\"MyButton\" parent=\"UIParent\"/></Ui>");
|
|
const EmitResult r = emitFrameXml(root);
|
|
REQUIRE(has(r.lua, "CreateFrame(\"Button\", \"MyButton\", UIParent)"));
|
|
}
|
|
|
|
TEST_CASE("Size and anchors become the same calls a script would make",
|
|
"[framexml][emit]") {
|
|
XmlNode root = parseOrFail(
|
|
"<Ui><Frame name=\"F\">"
|
|
"<Size><AbsDimension x=\"128\" y=\"64\"/></Size>"
|
|
"<Anchors><Anchor point=\"TOPLEFT\" relativePoint=\"BOTTOMRIGHT\">"
|
|
"<Offset><AbsDimension x=\"5\" y=\"-7\"/></Offset></Anchor></Anchors>"
|
|
"</Frame></Ui>");
|
|
const EmitResult r = emitFrameXml(root);
|
|
REQUIRE(has(r.lua, ":SetSize(128"));
|
|
REQUIRE(has(r.lua, ":SetPoint(\"TOPLEFT\""));
|
|
REQUIRE(has(r.lua, "\"BOTTOMRIGHT\""));
|
|
REQUIRE(has(r.lua, "5.000000, -7.000000"));
|
|
}
|
|
|
|
TEST_CASE("$parent expands against the frame that owns the region",
|
|
"[framexml][emit]") {
|
|
// Nearly every region in the original interface is named this way, and
|
|
// getting it wrong means none of them are reachable by name.
|
|
REQUIRE(substituteParent("$parentText", "FooFrame") == "FooFrameText");
|
|
REQUIRE(substituteParent("PlainName", "FooFrame") == "PlainName");
|
|
REQUIRE(substituteParent("", "FooFrame").empty());
|
|
|
|
XmlNode root = parseOrFail(
|
|
"<Ui><Frame name=\"FooFrame\"><Layers><Layer level=\"BACKGROUND\">"
|
|
"<Texture name=\"$parentBg\" file=\"Interface\\Foo\" setAllPoints=\"true\"/>"
|
|
"</Layer></Layers></Frame></Ui>");
|
|
const EmitResult r = emitFrameXml(root);
|
|
REQUIRE(has(r.lua, "CreateTexture(\"FooFrameBg\", \"BACKGROUND\")"));
|
|
REQUIRE(has(r.lua, "SetTexture(\"Interface\\\\Foo\")"));
|
|
REQUIRE(has(r.lua, ":SetAllPoints("));
|
|
}
|
|
|
|
TEST_CASE("alphaMode reaches the texture as a blend mode", "[framexml][emit]") {
|
|
// Art declared this way is a glow on black with no alpha channel of its
|
|
// own. Dropping the attribute draws it as an opaque black shape over
|
|
// whatever it was meant to light up.
|
|
XmlNode root = parseOrFail(
|
|
"<Ui><Frame name=\"FooFrame\"><Layers><Layer level=\"OVERLAY\">"
|
|
"<Texture name=\"$parentGlow\" file=\"Interface\\Glow\" alphaMode=\"ADD\"/>"
|
|
"</Layer></Layers></Frame></Ui>");
|
|
const EmitResult r = emitFrameXml(root);
|
|
REQUIRE(has(r.lua, ":SetBlendMode(\"ADD\")"));
|
|
}
|
|
|
|
TEST_CASE("A virtual frame becomes a template rather than a frame",
|
|
"[framexml][emit]") {
|
|
XmlNode root = parseOrFail(
|
|
"<Ui><Frame name=\"MyTemplate\" virtual=\"true\">"
|
|
"<Size><AbsDimension x=\"32\" y=\"32\"/></Size></Frame>"
|
|
"<Frame name=\"Real\" inherits=\"MyTemplate\"/></Ui>");
|
|
const EmitResult r = emitFrameXml(root);
|
|
|
|
REQUIRE(has(r.lua, "__WoweeTemplates[\"MyTemplate\"] = function(self)"));
|
|
// The template must not create a frame of its own; that is the whole
|
|
// meaning of virtual.
|
|
REQUIRE_FALSE(has(r.lua, "CreateFrame(\"Frame\", \"MyTemplate\""));
|
|
REQUIRE(has(r.lua, "CreateFrame(\"Frame\", \"Real\""));
|
|
REQUIRE(has(r.lua, "__WoweeTemplates[\"MyTemplate\"]("));
|
|
}
|
|
|
|
TEST_CASE("Inheriting several templates applies them in order", "[framexml][emit]") {
|
|
XmlNode root = parseOrFail("<Ui><Frame name=\"R\" inherits=\"A, B\"/></Ui>");
|
|
const EmitResult r = emitFrameXml(root);
|
|
const size_t a = r.lua.find("__WoweeTemplates[\"A\"](");
|
|
const size_t b = r.lua.find("__WoweeTemplates[\"B\"](");
|
|
REQUIRE(a != std::string::npos);
|
|
REQUIRE(b != std::string::npos);
|
|
REQUIRE(a < b);
|
|
}
|
|
|
|
TEST_CASE("Scripts bind both inline bodies and named functions",
|
|
"[framexml][emit]") {
|
|
XmlNode root = parseOrFail(
|
|
"<Ui><Frame name=\"F\"><Scripts>"
|
|
"<OnLoad><![CDATA[ self:SetAlpha(0.5) ]]></OnLoad>"
|
|
"<OnClick function=\"MyHandler\"/>"
|
|
"</Scripts></Frame></Ui>");
|
|
const EmitResult r = emitFrameXml(root);
|
|
REQUIRE(has(r.lua, ":SetScript(\"OnLoad\", function(self, ...)"));
|
|
REQUIRE(has(r.lua, "self:SetAlpha(0.5)"));
|
|
REQUIRE(has(r.lua, ":SetScript(\"OnClick\", MyHandler)"));
|
|
// OnLoad is expected to run once the frame is built, which is what every
|
|
// handler in FrameXML assumes about itself.
|
|
REQUIRE(has(r.lua, "GetScript(\"OnLoad\")"));
|
|
}
|
|
|
|
TEST_CASE("Referenced files are reported rather than loaded", "[framexml][emit]") {
|
|
XmlNode root = parseOrFail(
|
|
"<Ui><Script file=\"Foo.lua\"/><Include file=\"Bar.xml\"/></Ui>");
|
|
const EmitResult r = emitFrameXml(root);
|
|
REQUIRE(r.scriptFiles.size() == 1);
|
|
REQUIRE(r.scriptFiles[0] == "Foo.lua");
|
|
REQUIRE(r.includeFiles.size() == 1);
|
|
REQUIRE(r.includeFiles[0] == "Bar.xml");
|
|
}
|
|
|
|
TEST_CASE("Nested frames are parented to the frame containing them",
|
|
"[framexml][emit]") {
|
|
XmlNode root = parseOrFail(
|
|
"<Ui><Frame name=\"Outer\"><Frames>"
|
|
"<Button name=\"$parentBtn\"/>"
|
|
"</Frames></Frame></Ui>");
|
|
const EmitResult r = emitFrameXml(root);
|
|
REQUIRE(has(r.lua, "CreateFrame(\"Button\", \"OuterBtn\", __w[1])"));
|
|
}
|
|
|
|
TEST_CASE("Strings that reach Lua are escaped", "[framexml][emit]") {
|
|
// Texture paths are full of backslashes, and a quote in label text would
|
|
// otherwise end the string and leave the rest as code.
|
|
XmlNode root = parseOrFail(
|
|
"<Ui><Frame name=\"F\"><Layers><Layer>"
|
|
"<FontString text=\"He said "hi"\"/>"
|
|
"<Texture file=\"Interface\\Icons\\Foo\"/>"
|
|
"</Layer></Layers></Frame></Ui>");
|
|
const EmitResult r = emitFrameXml(root);
|
|
REQUIRE(has(r.lua, "\\\"hi\\\""));
|
|
REQUIRE(has(r.lua, "Interface\\\\Icons\\\\Foo"));
|
|
}
|
|
|
|
TEST_CASE("$parent inside a template resolves to the frame that inherits it",
|
|
"[framexml][emit]") {
|
|
// The subtlety that makes templates work at all. A region named $parentBg in
|
|
// a template must become FooFrameBg on the frame inheriting it, not
|
|
// TemplateNameBg — the template's own name is never the answer, and every
|
|
// frame sharing that template would collide on it if it were.
|
|
XmlNode root = parseOrFail(
|
|
"<Ui><Frame name=\"MyTemplate\" virtual=\"true\"><Layers><Layer>"
|
|
"<Texture name=\"$parentBg\" setAllPoints=\"true\"/>"
|
|
"</Layer></Layers></Frame></Ui>");
|
|
const EmitResult r = emitFrameXml(root);
|
|
|
|
REQUIRE(has(r.lua, "GetName()"));
|
|
REQUIRE(has(r.lua, "\"Bg\""));
|
|
REQUIRE_FALSE(has(r.lua, "\"MyTemplateBg\""));
|
|
}
|
|
|
|
TEST_CASE("A template installs OnLoad but does not run it", "[framexml][emit]") {
|
|
// A frame is loaded once, when it is finished — not once per template it
|
|
// is built from. Running it per template fired ChatFrameEditBoxTemplate's
|
|
// OnLoad before the edit box's own OnLoad had set self.chatFrame, which is
|
|
// the first thing that handler indexes.
|
|
XmlNode root = parseOrFail(
|
|
"<Ui><Frame name=\"T\" virtual=\"true\">"
|
|
"<Scripts><OnLoad>DoThing(self)</OnLoad></Scripts>"
|
|
"</Frame>"
|
|
"<Frame name=\"Real\" inherits=\"T\"/></Ui>");
|
|
const EmitResult r = emitFrameXml(root);
|
|
|
|
// Once, for the real frame — not inside the template body.
|
|
const std::string fire = ":GetScript(\"OnLoad\")(";
|
|
size_t count = 0;
|
|
for (size_t at = r.lua.find(fire); at != std::string::npos;
|
|
at = r.lua.find(fire, at + 1)) ++count;
|
|
REQUIRE(count == 1);
|
|
REQUIRE(has(r.lua, "SetScript(\"OnLoad\""));
|
|
}
|
|
|
|
TEST_CASE("$parent skips unnamed frames to the nearest named one",
|
|
"[framexml][emit]") {
|
|
// An unnamed frame has no name to lend, so $parent means the nearest
|
|
// ancestor that has one. PartyMemberPetFrameTemplate buries its
|
|
// $parentName two unnamed frames deep and expects
|
|
// PartyMemberFrame1PetFrameName; asking the unnamed frame gave nil and
|
|
// named the region "Name", which nothing was looking for.
|
|
XmlNode root = parseOrFail(
|
|
"<Ui><Frame name=\"T\" virtual=\"true\"><Frames>"
|
|
"<Frame><Frames><Frame>"
|
|
"<Layers><Layer><FontString name=\"$parentName\"/></Layer></Layers>"
|
|
"</Frame></Frames></Frame>"
|
|
"</Frames></Frame></Ui>");
|
|
const EmitResult r = emitFrameXml(root);
|
|
|
|
// Named from the template root, which is the only thing with a name.
|
|
REQUIRE(has(r.lua, "((self:GetName() or \"\") .. \"Name\")"));
|
|
}
|
|
|
|
TEST_CASE("A frame can fill its parent instead of anchoring", "[framexml][emit]") {
|
|
// Honoured for regions and ignored for frames, which is 139 declarations
|
|
// across 53 files. An unanchored frame falls to the centre-on-parent
|
|
// default with no size, so its centre is the screen's — PlayerFrame's name
|
|
// sat in the middle of the world because two frames above it said
|
|
// setAllPoints and nothing acted on it.
|
|
XmlNode root = parseOrFail(
|
|
"<Ui><Frame name=\"Outer\"><Frames>"
|
|
"<Frame name=\"$parentInner\" setAllPoints=\"true\"/>"
|
|
"</Frames></Frame></Ui>");
|
|
const EmitResult r = emitFrameXml(root);
|
|
REQUIRE(has(r.lua, ":SetAllPoints(__w[1])"));
|
|
}
|
|
|
|
TEST_CASE("A Font element becomes a font object", "[framexml][emit]") {
|
|
// Not a widget: a named set of type settings that font strings inherit by
|
|
// name, and SetFontObject reads height and colour off it. FrameXML defines
|
|
// 42 of them and every label inherits one.
|
|
XmlNode root = parseOrFail(
|
|
"<Ui>"
|
|
"<Font name=\"Base\" font=\"Fonts\\\\FRIZQT__.TTF\" virtual=\"true\">"
|
|
"<FontHeight><AbsValue val=\"10\"/></FontHeight>"
|
|
"<Color r=\"1\" g=\"0.8\" b=\"0\"/>"
|
|
"</Font>"
|
|
"<Font name=\"Derived\" inherits=\"Base\" virtual=\"true\">"
|
|
"<FontHeight><AbsValue val=\"16\"/></FontHeight>"
|
|
"</Font>"
|
|
"</Ui>");
|
|
const EmitResult r = emitFrameXml(root);
|
|
|
|
REQUIRE(has(r.lua, "Base.height = 10"));
|
|
REQUIRE(has(r.lua, "Base.r = 1"));
|
|
// Copied first, so the height stated here wins over the inherited one.
|
|
REQUIRE(has(r.lua, "for k, v in pairs(base) do Derived[k] = v end"));
|
|
REQUIRE(r.lua.find("pairs(base) do Derived") < r.lua.find("Derived.height = 16"));
|
|
// A font object is not a frame.
|
|
REQUIRE_FALSE(has(r.lua, "CreateFrame(\"Font\""));
|
|
}
|
|
|
|
TEST_CASE("A slider carries its range, step and grip", "[framexml][emit]") {
|
|
// The range is set before the value, or the value is clamped against a
|
|
// default range it was never meant to sit in. The thumb is a region like
|
|
// button art, handed to the setter afterwards.
|
|
XmlNode root = parseOrFail(
|
|
"<Ui><Slider name=\"S\" minValue=\"0\" maxValue=\"100\" valueStep=\"5\""
|
|
" defaultValue=\"20\" orientation=\"VERTICAL\">"
|
|
"<ThumbTexture name=\"$parentThumb\" file=\"Art\\\\Grip\"/>"
|
|
"</Slider></Ui>");
|
|
const EmitResult r = emitFrameXml(root);
|
|
|
|
REQUIRE(has(r.lua, ":SetMinMaxValues("));
|
|
REQUIRE(has(r.lua, ":SetValueStep("));
|
|
REQUIRE(has(r.lua, ":SetOrientation(\"VERTICAL\")"));
|
|
REQUIRE(has(r.lua, ":SetThumbTexture("));
|
|
REQUIRE(r.lua.find(":SetMinMaxValues(") < r.lua.find(":SetValue("));
|
|
}
|
|
|
|
TEST_CASE("A frame's id becomes SetID", "[framexml][emit]") {
|
|
// How a frame in a numbered set knows which one it is. FrameXML builds
|
|
// names out of it — PartyMemberFrame_RefreshPetDebuffs reaches for
|
|
// _G["PartyMemberFrame" .. self:GetID() .. "PetFrame"] — and 848 of these
|
|
// are declared across 57 files.
|
|
XmlNode root = parseOrFail("<Ui><Frame name=\"F\" id=\"3\"/></Ui>");
|
|
REQUIRE(has(emitFrameXml(root).lua, ":SetID(3)"));
|
|
}
|
|
|
|
TEST_CASE("parentKey binds a region to a field on its owner", "[framexml][emit]") {
|
|
// How FrameXML's handlers reach their own pieces: QuestHonorFrameTemplate's
|
|
// OnLoad opens with self.icon:SetTexture(...), and the icon is bound only
|
|
// by parentKey="icon" on the texture. Ignoring the attribute left all 242
|
|
// of those fields nil across 31 files.
|
|
XmlNode root = parseOrFail(
|
|
"<Ui><Frame name=\"Panel\">"
|
|
"<Layers><Layer><Texture name=\"$parentIcon\" parentKey=\"icon\"/></Layer></Layers>"
|
|
"<Frames><Frame name=\"$parentBar\" parentKey=\"bar\"/></Frames>"
|
|
"</Frame></Ui>");
|
|
const EmitResult r = emitFrameXml(root);
|
|
|
|
REQUIRE(has(r.lua, "[\"icon\"] = "));
|
|
REQUIRE(has(r.lua, "[\"bar\"] = "));
|
|
}
|
|
|
|
TEST_CASE("A template that inherits another applies it too", "[framexml][emit]") {
|
|
// Templates are built from other templates constantly — 217 of FrameXML's
|
|
// virtual frames inherit one — and the virtual branch used to return before
|
|
// inherits was ever emitted. InterfaceOptionsListButtonTemplate silently
|
|
// dropped the OptionsListButtonTemplate it is built on, so it arrived with
|
|
// no highlight texture and no size.
|
|
XmlNode root = parseOrFail(
|
|
"<Ui><Button name=\"Derived\" inherits=\"Base\" virtual=\"true\">"
|
|
"<Scripts><OnClick function=\"Foo\"/></Scripts>"
|
|
"</Button></Ui>");
|
|
const EmitResult r = emitFrameXml(root);
|
|
|
|
REQUIRE(has(r.lua, "__WoweeTemplates[\"Derived\"] = function(self)"));
|
|
REQUIRE(has(r.lua, "__WoweeTemplates[\"Base\"](self)"));
|
|
// Before the body, so the template's own settings win over the base's.
|
|
REQUIRE(r.lua.find("__WoweeTemplates[\"Base\"](self)") <
|
|
r.lua.find("SetScript(\"OnClick\""));
|
|
}
|
|
|
|
TEST_CASE("A frame's own $parent anchor means its parent, not itself",
|
|
"[framexml][emit]") {
|
|
// A sibling reference. VideoOptionsFrameCancel anchors to $parentApply,
|
|
// meaning the Apply button beside it on the frame holding both — not a
|
|
// child of the Cancel button. Resolving it against the button's own name
|
|
// produced VideoOptionsFrameCancelApply, which nothing is called, so the
|
|
// anchor silently fell back to the parent and the button sat in the wrong
|
|
// place. Regions are the other way round and must keep working.
|
|
XmlNode root = parseOrFail(
|
|
"<Ui><Frame name=\"Panel\"><Frames>"
|
|
"<Button name=\"$parentCancel\"><Anchors>"
|
|
"<Anchor point=\"BOTTOMRIGHT\" relativeTo=\"$parentApply\"/>"
|
|
"</Anchors></Button>"
|
|
"</Frames><Layers><Layer>"
|
|
"<Texture name=\"$parentBg\"/>"
|
|
"</Layer></Layers></Frame></Ui>");
|
|
const EmitResult r = emitFrameXml(root);
|
|
|
|
REQUIRE(has(r.lua, "\"PanelApply\""));
|
|
REQUIRE_FALSE(has(r.lua, "\"PanelCancelApply\""));
|
|
// The region still names itself after the frame that owns it.
|
|
REQUIRE(has(r.lua, "\"PanelBg\""));
|
|
}
|
|
|
|
TEST_CASE("Button art declared outside a Layer is still created",
|
|
"[framexml][emit]") {
|
|
// <NormalTexture> and <ButtonText> are regions like any other, just
|
|
// declared as their own element with an implied layer and a setter. The
|
|
// emitter ignored all of them, so the names they declare never existed —
|
|
// _G["DropDownList1Button1NormalText"] among them, which is what stopped
|
|
// UIDropDownMenu loading. The highlight belongs on its own layer, and the
|
|
// label above the art rather than under it.
|
|
XmlNode root = parseOrFail(
|
|
"<Ui><Button name=\"MyButton\">"
|
|
"<NormalTexture name=\"$parentNormalTexture\" file=\"Art\\\\Face\"/>"
|
|
"<HighlightTexture name=\"$parentHighlight\"/>"
|
|
"<ButtonText name=\"$parentNormalText\"/>"
|
|
"</Button></Ui>");
|
|
const EmitResult r = emitFrameXml(root);
|
|
|
|
REQUIRE(has(r.lua, "\"MyButtonNormalTexture\""));
|
|
REQUIRE(has(r.lua, "\"MyButtonNormalText\""));
|
|
REQUIRE(has(r.lua, ":SetNormalTexture("));
|
|
REQUIRE(has(r.lua, ":SetFontString("));
|
|
REQUIRE(has(r.lua, "\"HIGHLIGHT\""));
|
|
REQUIRE(has(r.lua, "\"OVERLAY\""));
|
|
// A font string, not a texture — the element name does not say so.
|
|
REQUIRE(has(r.lua, "CreateFontString(\"MyButtonNormalText\""));
|
|
}
|
|
|
|
TEST_CASE("A nested frame in a template also resolves $parent at replay time",
|
|
"[framexml][emit]") {
|
|
// The same rule as the region above, and it was the region that had it. A
|
|
// child frame named $parentScrollBar was emitted with the template's own
|
|
// name baked in, so every scroll frame in FrameXML created a global called
|
|
// UIPanelScrollFrameTemplateScrollBar and overwrote the last one, while the
|
|
// _G[self:GetName().."ScrollBar"] its handlers look up never existed. That
|
|
// one line accounted for most of the files that would not load.
|
|
XmlNode root = parseOrFail(
|
|
"<Ui><Frame name=\"MyTemplate\" virtual=\"true\"><Frames>"
|
|
"<Frame name=\"$parentScrollBar\"/>"
|
|
"</Frames></Frame></Ui>");
|
|
const EmitResult r = emitFrameXml(root);
|
|
|
|
REQUIRE(has(r.lua, "GetName()"));
|
|
REQUIRE(has(r.lua, "\"ScrollBar\""));
|
|
REQUIRE_FALSE(has(r.lua, "\"MyTemplateScrollBar\""));
|
|
}
|
|
|
|
TEST_CASE("Outside a template $parent is resolved when emitted",
|
|
"[framexml][emit]") {
|
|
// No reason to defer it where the owning frame is already known; a literal
|
|
// keeps the generated code readable.
|
|
XmlNode root = parseOrFail(
|
|
"<Ui><Frame name=\"FooFrame\"><Layers><Layer>"
|
|
"<Texture name=\"$parentBg\"/></Layer></Layers></Frame></Ui>");
|
|
const EmitResult r = emitFrameXml(root);
|
|
REQUIRE(has(r.lua, "\"FooFrameBg\""));
|
|
REQUIRE_FALSE(has(r.lua, "GetName()"));
|
|
}
|
|
|
|
TEST_CASE("A nested frame anchors to its container, not the screen",
|
|
"[framexml][emit]") {
|
|
// FrameXML nests constantly, and an unqualified anchor means "my parent".
|
|
// Reading it as UIParent puts anything inside a panel somewhere else on the
|
|
// screen entirely.
|
|
XmlNode root = parseOrFail(
|
|
"<Ui><Frame name=\"Outer\"><Frames>"
|
|
"<StatusBar name=\"$parentBar\"><Anchors>"
|
|
"<Anchor point=\"BOTTOMLEFT\" relativePoint=\"BOTTOMLEFT\">"
|
|
"<Offset><AbsDimension x=\"5\" y=\"5\"/></Offset></Anchor>"
|
|
"</Anchors></StatusBar>"
|
|
"</Frames></Frame></Ui>");
|
|
const EmitResult r = emitFrameXml(root);
|
|
REQUIRE(has(r.lua, "CreateFrame(\"StatusBar\", \"OuterBar\", __w[1])"));
|
|
REQUIRE(has(r.lua, ":SetPoint(\"BOTTOMLEFT\", __w[1],"));
|
|
REQUIRE_FALSE(has(r.lua, ":SetPoint(\"BOTTOMLEFT\", UIParent,"));
|
|
}
|
|
|
|
TEST_CASE("An anchor inside a template resolves its parent at replay time",
|
|
"[framexml][emit]") {
|
|
// The containing frame is not known while emitting a template — it is
|
|
// whichever frame inherits it — so the parent has to be asked for then.
|
|
XmlNode root = parseOrFail(
|
|
"<Ui><Frame name=\"T\" virtual=\"true\"><Anchors>"
|
|
"<Anchor point=\"CENTER\"/></Anchors></Frame></Ui>");
|
|
const EmitResult r = emitFrameXml(root);
|
|
REQUIRE(has(r.lua, "self:GetParent()"));
|
|
}
|
|
|
|
TEST_CASE("A FontString's inherits names a font object, not a template",
|
|
"[framexml][emit]") {
|
|
// Frames inherit templates; FontStrings inherit shared font objects, and
|
|
// that is where their size and colour come from. FrameXML does it more than
|
|
// three thousand times, so treating it as a template would leave every
|
|
// label the same size in the same colour.
|
|
//
|
|
// It is not either/or, though: FrameXML declares virtual FontStrings too,
|
|
// and the name alone does not say which kind it is. So the emitted line
|
|
// asks — but a font object must still reach SetFontObject, which is what
|
|
// this checks.
|
|
XmlNode root = parseOrFail(
|
|
"<Ui><Frame name=\"F\"><Layers><Layer>"
|
|
"<FontString name=\"$parentT\" inherits=\"GameFontNormalLarge\" text=\"Hi\"/>"
|
|
"</Layer></Layers></Frame></Ui>");
|
|
const EmitResult r = emitFrameXml(root);
|
|
REQUIRE(has(r.lua, ":SetFontObject(\"GameFontNormalLarge\")"));
|
|
REQUIRE(has(r.lua, "else "));
|
|
}
|
|
|
|
TEST_CASE("A virtual Texture becomes a template a region can inherit",
|
|
"[framexml][emit]") {
|
|
// Twenty-two of these sit at the top level of FrameXML — the dialog
|
|
// button's normal, pushed and highlight art among them. None was emitted,
|
|
// so every button inheriting its art had none.
|
|
XmlNode root = parseOrFail(
|
|
"<Ui>"
|
|
"<Texture name=\"DialogButtonNormalTexture\" file=\"Interface\\Up\" virtual=\"true\"/>"
|
|
"<Frame name=\"F\"><Layers><Layer>"
|
|
"<Texture name=\"$parentArt\" inherits=\"DialogButtonNormalTexture\"/>"
|
|
"</Layer></Layers></Frame></Ui>");
|
|
const EmitResult r = emitFrameXml(root);
|
|
REQUIRE(has(r.lua, "__WoweeTemplates[\"DialogButtonNormalTexture\"] = function(self)"));
|
|
REQUIRE(has(r.lua, "self:SetTexture(\"Interface\\\\Up\")"));
|
|
REQUIRE(has(r.lua, "__WoweeTemplates[\"DialogButtonNormalTexture\"](__w[2])"));
|
|
}
|
|
|
|
TEST_CASE("A Texture's inherits is not treated as a font object",
|
|
"[framexml][emit]") {
|
|
XmlNode root = parseOrFail(
|
|
"<Ui><Frame name=\"F\"><Layers><Layer>"
|
|
"<Texture name=\"$parentTex\" inherits=\"SomeTextureTemplate\"/>"
|
|
"</Layer></Layers></Frame></Ui>");
|
|
const EmitResult r = emitFrameXml(root);
|
|
REQUIRE_FALSE(has(r.lua, "SetFontObject"));
|
|
}
|
|
|
|
TEST_CASE("Handler bodies get their arguments by name", "[framexml][emit]") {
|
|
// Blizzard's inline scripts use their argument names without declaring
|
|
// them. Passed positionally instead, an OnUpdate body's `elapsed` is nil
|
|
// and the first arithmetic on it fails — which is most of FrameXML's
|
|
// OnUpdate handlers.
|
|
XmlNode root = parseOrFail(
|
|
"<Ui><Frame name=\"F\"><Scripts>"
|
|
"<OnUpdate><![CDATA[ self.t = (self.t or 0) + elapsed ]]></OnUpdate>"
|
|
"<OnClick><![CDATA[ if button == \"LeftButton\" then self:Hide() end ]]></OnClick>"
|
|
"<OnEvent><![CDATA[ if event == \"PLAYER_LOGIN\" then self:Show() end ]]></OnEvent>"
|
|
"<OnValueChanged><![CDATA[ self:SetAlpha(value) ]]></OnValueChanged>"
|
|
"</Scripts></Frame></Ui>");
|
|
const EmitResult r = emitFrameXml(root);
|
|
|
|
REQUIRE(has(r.lua, "function(self, elapsed, ...)"));
|
|
REQUIRE(has(r.lua, "function(self, button, down, ...)"));
|
|
REQUIRE(has(r.lua, "function(self, event,"));
|
|
REQUIRE(has(r.lua, "function(self, value, ...)"));
|
|
REQUIRE_FALSE(has(r.lua, "local arg1, arg2, arg3, arg4 = ..."));
|
|
}
|
|
|
|
TEST_CASE("A handler with no named arguments still takes self",
|
|
"[framexml][emit]") {
|
|
XmlNode root = parseOrFail(
|
|
"<Ui><Frame name=\"F\"><Scripts>"
|
|
"<OnShow><![CDATA[ self:SetAlpha(1) ]]></OnShow>"
|
|
"</Scripts></Frame></Ui>");
|
|
const EmitResult r = emitFrameXml(root);
|
|
REQUIRE(has(r.lua, "function(self, ...)"));
|
|
}
|
|
|
|
TEST_CASE("Every handler is vararg whatever its named arguments",
|
|
"[framexml][emit]") {
|
|
// A body is free to use `...` whatever handler it belongs to, and a
|
|
// parameter list without it does not merely lose the values — it fails to
|
|
// compile, taking the whole template with it.
|
|
XmlNode root = parseOrFail(
|
|
"<Ui><Frame name=\"F\"><Scripts>"
|
|
"<OnEvent><![CDATA[ local a, b = ...; self:SetAlpha(1) ]]></OnEvent>"
|
|
"<OnUpdate><![CDATA[ local x = select(1, ...) ]]></OnUpdate>"
|
|
"</Scripts></Frame></Ui>");
|
|
const EmitResult r = emitFrameXml(root);
|
|
REQUIRE(has(r.lua, "function(self, elapsed, ...)"));
|
|
REQUIRE(has(r.lua, "function(self, event,"));
|
|
// Both signatures end in varargs.
|
|
size_t at = 0, sigs = 0;
|
|
while ((at = r.lua.find(", ...)", at)) != std::string::npos) { ++sigs; at += 5; }
|
|
REQUIRE(sigs >= 2);
|
|
}
|
|
|
|
TEST_CASE("A $parent relativeTo becomes a name, not a bare symbol",
|
|
"[framexml][emit]") {
|
|
// $parentBg is not a Lua identifier. Pasted in as one it is a syntax error,
|
|
// which does not lose the anchor — it loses the whole file.
|
|
XmlNode root = parseOrFail(
|
|
"<Ui><Frame name=\"FooFrame\"><Layers><Layer>"
|
|
"<Texture name=\"$parentBg\"/>"
|
|
"<Texture name=\"$parentIcon\"><Anchors>"
|
|
"<Anchor point=\"LEFT\" relativeTo=\"$parentBg\" relativePoint=\"RIGHT\"/>"
|
|
"</Anchors></Texture>"
|
|
"</Layer></Layers></Frame></Ui>");
|
|
const EmitResult r = emitFrameXml(root);
|
|
REQUIRE_FALSE(has(r.lua, "$parent"));
|
|
REQUIRE(has(r.lua, "\"FooFrameBg\""));
|
|
}
|
|
|
|
TEST_CASE("Temporaries do not run into Lua's local-variable limit",
|
|
"[framexml][emit]") {
|
|
// Lua allows 200 locals per function. A large file declares far more
|
|
// widgets than that, and going over does not degrade — the whole chunk
|
|
// refuses to compile. FriendsFrame and InterfaceOptionsPanels both did.
|
|
std::string xml = "<Ui><Frame name=\"Big\"><Layers><Layer>";
|
|
for (int i = 0; i < 300; ++i) {
|
|
xml += "<Texture name=\"$parentT" + std::to_string(i) + "\"/>";
|
|
}
|
|
xml += "</Layer></Layers></Frame></Ui>";
|
|
XmlNode root = parseOrFail(xml);
|
|
const EmitResult r = emitFrameXml(root);
|
|
|
|
// One table, not three hundred locals.
|
|
REQUIRE(has(r.lua, "local __w = {}"));
|
|
size_t locals = 0, at = 0;
|
|
while ((at = r.lua.find("local ", at)) != std::string::npos) { ++locals; at += 6; }
|
|
REQUIRE(locals == 1);
|
|
}
|
|
|
|
TEST_CASE("An empty function attribute is not emitted as a handler name",
|
|
"[framexml][emit]") {
|
|
// SetScript("X", ) is a syntax error, so this loses the whole file rather
|
|
// than the one handler.
|
|
XmlNode root = parseOrFail(
|
|
"<Ui><Frame name=\"F\"><Scripts>"
|
|
"<OnMouseWheel function=\"\"/>"
|
|
"<OnShow function=\"RealHandler\"/>"
|
|
"</Scripts></Frame></Ui>");
|
|
const EmitResult r = emitFrameXml(root);
|
|
REQUIRE_FALSE(has(r.lua, "SetScript(\"OnMouseWheel\", )"));
|
|
REQUIRE(has(r.lua, "SetScript(\"OnShow\", RealHandler)"));
|
|
}
|
|
|
|
TEST_CASE("$parent follows the parent attribute, not the file structure",
|
|
"[framexml][emit]") {
|
|
// A frame written at the top level with parent="Something" belongs to that
|
|
// frame, and its $parent means it. WorldMapTitleButton is declared this way
|
|
// and anchors to $parentMiniBorderLeft — the world map's own border. Taken
|
|
// from where it sits in the file instead, there is no containing frame to
|
|
// name, so $parent collapsed to the bare suffix and SetPoint looked up a
|
|
// global that nothing has.
|
|
XmlNode root = parseOrFail(
|
|
"<Ui>"
|
|
"<Frame name=\"Panel\"><Layers><Layer>"
|
|
"<Texture name=\"$parentEdge\" file=\"Interface\\Edge\"/>"
|
|
"</Layer></Layers></Frame>"
|
|
"<Button name=\"PanelTitle\" parent=\"Panel\">"
|
|
"<Anchors><Anchor point=\"TOPLEFT\" relativeTo=\"$parentEdge\"/></Anchors>"
|
|
"</Button></Ui>");
|
|
const EmitResult r = emitFrameXml(root);
|
|
REQUIRE(has(r.lua, "CreateTexture(\"PanelEdge\""));
|
|
REQUIRE(has(r.lua, ":SetPoint(\"TOPLEFT\", \"PanelEdge\""));
|
|
REQUIRE_FALSE(has(r.lua, "\"Edge\""));
|
|
}
|