0.1.22
This commit is contained in:
@@ -5,6 +5,12 @@
|
||||
### Fixed
|
||||
|
||||
- Manifest assets that share the same id under different types are now all kept in the by-id index. Previously the first same-id entry (e.g. `W3DHierarchy:AUMCV_HOVER`) could shadow later definitions (e.g. `W3DContainer:AUMCV_HOVER`), causing `Model@Name` and other `BaseRenderAssetType` references to be reported as unresolved even though the asset existed in `Static.manifest`.
|
||||
- `xi:include` without an `xpointer` now splices the target document's root element itself (XInclude semantics), so fragments like `GenericCelestialBuildingSuicide.xml` keep their module wrapper (`CreateObjectDie`) instead of only inserting its children.
|
||||
- Fragment files (documents whose root is not `AssetDeclaration`) no longer trigger standalone-document diagnostics: top-level `missing-id`, duplicate-id, unresolved-reference and undefined-define checks are skipped, unknown wrapper roots are not reported as unknown elements, and a known fragment root still validates its subtree's elements/attributes.
|
||||
|
||||
### Added
|
||||
|
||||
- Missing `xi:include` targets now surface in the Problems panel as `include-not-found` warnings for the edited document (previously only tracked in indexer diagnostics).
|
||||
|
||||
## 0.1.21 — 2026-08-11
|
||||
|
||||
|
||||
@@ -1906,3 +1906,55 @@ W3DContainer:AUMCV_HOVER
|
||||
|
||||
`docs/plan.md` 的 manifest 建模小节补充:`assetsById` 必须保留同 id 的不同
|
||||
类型定义,去重身份为 `(type, file, line)`。
|
||||
|
||||
---
|
||||
|
||||
## 二十八、问题分析(2026-08-11):xi:include 无 xpointer 语义与片段文件诊断(P0)
|
||||
|
||||
### 现象
|
||||
|
||||
`Data/Includes/GenericCelestialBuildingSuicide.xml` 这类被 `xi:include` 引用的
|
||||
片段文件在独立打开时被报一串错误:`DieMuxData` 报 `missing-id`(“顶层资产需要
|
||||
id”),wrapper 根不在 XSD 里的文件报 `unknown-element`,引用在完整索引下能解析
|
||||
前还会报未解析引用。
|
||||
|
||||
### 根因
|
||||
|
||||
1. **无 `xpointer` 的展开语义错误**:`expandDocument` 把目标 `root.children`
|
||||
拼进父节点。按 XInclude 语义(也是 Corona 的实际用法),没有 `xpointer` 时应
|
||||
整体包含目标文档的根元素。`GenericCelestialBuildingSuicide.xml` 的根
|
||||
`CreateObjectDie` 本身就是要放进 GameObject 的模块;旧实现会丢掉它,只把
|
||||
`DieMuxData` 拼进去。
|
||||
2. **诊断层把片段当完整文档**:`isTopLevel` 假定根一定是 `AssetDeclaration`,
|
||||
于是片段根的子元素被当成顶层资产要求 id;未知 wrapper 根也被当成未知元素。
|
||||
3. **引用/define 与上下文耦合**:片段里的引用可能由 include 者(或 include 者
|
||||
的 include 链)提供,独立打开片段时无法可靠判定。
|
||||
|
||||
### 修复(P0,不猜测外部上下文)
|
||||
|
||||
1. `logicalTree.expandDocument`:无 `xpointer` 时 `handleChild(parse.root)`,
|
||||
整体包含目标根元素;有 `xpointer` 时保持 `/n:Name/child::*` 语义。
|
||||
2. `diagnostics` 片段模式:根 localName 不是 `AssetDeclaration` 即为片段。
|
||||
- 一律跳过顶层 `missing-id` / 跨文件重复 id、未解析引用、未定义 `$DEFINE`;
|
||||
- 根是已知 XSD 元素时(如 `CreateObjectDie`),根自身提供类型上下文,整棵子树
|
||||
的未知元素 / 未知属性仍正常校验;
|
||||
- 根不在 XSD 中(wrapper/container,如 `CommonArmorDraws`)时,只报 XML 语法
|
||||
与片段内部 `xi:include` / `<Include>` 目标缺失,其余检查延后到上下文诊断。
|
||||
3. 新增 `checkXiInclude`:`xi:include` 目标缺失在 Problems 中上报
|
||||
`include-not-found`(此前只在 indexer 内部诊断)。
|
||||
|
||||
### 测试(202 → 202 全绿)
|
||||
|
||||
- `localScope.test.mjs`:无 `xpointer` 的 `xi:include` 把目标根元素
|
||||
`CreateObjectDie` 整体拼入 `Behaviors`,`DieMuxData` 仍挂在它下面;
|
||||
- `contentFeatures.test.mjs`:片段已知根不再报 `missing-id` / 未解析引用,但子树
|
||||
未知属性仍报;未知 wrapper 根不报元素/属性,片段内部缺失 `xi:include` 仍报;
|
||||
完整 `AssetDeclaration` 文档的顶层 id 检查不受影响。
|
||||
|
||||
### 边界与后续
|
||||
|
||||
- **P1 上下文诊断**:indexer 增加“反向 include 表”(`xi:include` 目标 →
|
||||
include 者列表),打开片段时用 include 者的逻辑树做真实上下文校验,再恢复引用 /
|
||||
define / 子元素结构检查。多上下文取并集去重。
|
||||
- `<Include type="all|instance|reference">` 与 `xi:include` 语义不同:前者的目标
|
||||
是完整 `AssetDeclaration`,不进入片段模式;后者才允许片段文件。
|
||||
|
||||
+9
-6
@@ -464,7 +464,7 @@ test/
|
||||
展开(第十二轮,见第六节);顶层 `<Include type="all">` 与 `inheritFrom` +
|
||||
`xai:joinAction` 的深合并仍未实现,后续如需要“当前文档视角的全量合并诊断”再继续。
|
||||
|
||||
## 六、include 展开设计备忘(2026-08-01;xi:include 部分已实施于第十二轮)
|
||||
## 六、include 展开设计备忘(2026-08-01;xi:include 部分已实施于第十二轮,无 xpointer 语义与片段诊断见第二十八轮)
|
||||
|
||||
> 目的:集中记录 include 处理相关的现状、结论与设计,下次遇到 include 问题时从这里继续,
|
||||
> 并在实施后把结果回写本节。
|
||||
@@ -477,7 +477,8 @@ test/
|
||||
| `reference` → builtmods manifest 解析 / 缺失回退占位 XML | 已实现 |
|
||||
| 嵌套 `xi:include`(任意层级):目标可索引、缺失报 `include-not-found`、Ctrl+点击跳转、`href` hover 解析目标 | 已实现(第二轮 + 第五轮) |
|
||||
| `xi:include` 及其属性不参与 XSD 校验(外来命名空间守卫 `isXsdElementName` / `isXsdAttributeName`) | 已实现(第五轮) |
|
||||
| include 目标内容“虚拟合并”进父文档的逻辑树 | 已实现 `xi:include`(第十二轮);顶层 `<Include type="all">` 仍不展开 |
|
||||
| include 目标内容“虚拟合并”进父文档的逻辑树 | 已实现 `xi:include`(第十二轮);**无 `xpointer` 时整体包含目标根元素**(第二十八轮修正);顶层 `<Include type="all">` 仍不展开 |
|
||||
| 片段文件(根非 `AssetDeclaration`)的诊断 | 已实现 P0(第二十八轮):跳过顶层 id/重复/引用/define 检查;根为已知 XSD 元素时仍校验子树;根未知时只报语法与 include 缺失 |
|
||||
|
||||
### 2. 已确认的方向
|
||||
|
||||
@@ -488,9 +489,11 @@ BAB(`defaultscript.cs`)编译时正是这样把整个 Mod 合并成一份大
|
||||
|
||||
- **不要**把 include 目标展开成文本再整体重新解析:源码偏移会断裂,诊断 / 跳转 / hover /
|
||||
补全全部无法映射回原始文件。
|
||||
- **要做**的是:解析器逐文件解析(现状不变);展开器把目标文件选中节点按 `xpointer`
|
||||
子集挂进父元素,节点保留各自的源文件与原始偏移(来源追溯)。后续分析跑在逻辑树上,
|
||||
范围映射按节点 `sourceFile` 回到对应文件的 lineMap。
|
||||
- **要做**的是:解析器逐文件解析(现状不变);展开器把目标文件选中节点挂进父元素——
|
||||
有 `xpointer` 时取 `/n:Name/child::*` 选中 children,无 `xpointer` 时按 XInclude 语义
|
||||
整体包含目标根元素(RA3 片段如 `CreateObjectDie` 依赖这一行为);节点保留各自的源文件
|
||||
与原始偏移(来源追溯)。后续分析跑在逻辑树上,范围映射按节点 `sourceFile` 回到对应
|
||||
文件的 lineMap。
|
||||
- 现有 `parseXml` 已记录标签 / 属性 / 值的起止偏移,`XmlElement` 结构可直接复用;拼接时
|
||||
用浅拷贝节点壳并重建 parent 链,避免破坏目标文件缓存树自身的 parent 指针。
|
||||
|
||||
@@ -498,7 +501,7 @@ BAB(`defaultscript.cs`)编译时正是这样把整个 Mod 合并成一份大
|
||||
|
||||
| 构造 | 拼入逻辑树 | 理由 |
|
||||
|---|---|---|
|
||||
| `xi:include` | ✅ | 内容并入父元素(HeadlightDraw2 场景) |
|
||||
| `xi:include` | ✅ | 有 `xpointer`:选中容器 children;无 `xpointer`:整体包含目标根元素(CreateObjectDie / TechUpgradeReceiver 等片段场景) |
|
||||
| EA `<Include type="all">` | ✅ | BAB“内容合并”,等价于复制进来 |
|
||||
| `type="instance"` | ❌ | 只影响编译可见性;拼树会把 BaseVehicle 的顶层资产错误塞进当前文档 |
|
||||
| `type="reference"` | ❌ | manifest 编译产物,无文本内容 |
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
" (based on a partial index)": " (based on a partial index)",
|
||||
" (index incomplete — may be a false positive)": " (index incomplete — may be a false positive)",
|
||||
"Include target not found: {0}": "Include target not found: {0}",
|
||||
"xi:include target not found: {0}": "xi:include target not found: {0}",
|
||||
"XInclude element (W3C XInclude namespace) — not part of the RA3 XSD model.": "XInclude element (W3C XInclude namespace) — not part of the RA3 XSD model.",
|
||||
"**Top-level asset element**": "**Top-level asset element**",
|
||||
"Attributes: {0} · Children: {1}": "Attributes: {0} · Children: {1}",
|
||||
|
||||
@@ -66,6 +66,7 @@
|
||||
" (based on a partial index)": "(基于部分索引)",
|
||||
" (index incomplete — may be a false positive)": "(索引不完整——可能是误报)",
|
||||
"Include target not found: {0}": "找不到 Include 目标:{0}",
|
||||
"xi:include target not found: {0}": "找不到 xi:include 目标:{0}",
|
||||
"XInclude element (W3C XInclude namespace) — not part of the RA3 XSD model.": "XInclude 元素(W3C XInclude 命名空间)——不属于 RA3 XSD 模型。",
|
||||
"**Top-level asset element**": "**顶层资产元素**",
|
||||
"Attributes: {0} · Children: {1}": "属性:{0} · 子元素:{1}",
|
||||
|
||||
+1
-1
@@ -2,7 +2,7 @@
|
||||
"name": "ra3-mod-xml",
|
||||
"displayName": "%ra3modxml.displayName%",
|
||||
"description": "%ra3modxml.description%",
|
||||
"version": "0.1.21",
|
||||
"version": "0.1.22",
|
||||
"publisher": "lanyi",
|
||||
"license": "SEE LICENSE IN LICENSE",
|
||||
"repository": {
|
||||
|
||||
+68
-14
@@ -139,6 +139,17 @@ export class Ra3Diagnostics {
|
||||
): void {
|
||||
const settings = this.ws.settings;
|
||||
const fileDuplicates = new Map<string, { line: number }>();
|
||||
// A file whose root is not AssetDeclaration is an xi:include fragment
|
||||
// (e.g. Data/Includes/GenericCelestialBuildingSuicide.xml). It is not a
|
||||
// standalone RA3 document: top-level id / duplicate checks do not apply,
|
||||
// and references/defines can only be resolved in the includer's context.
|
||||
// When the fragment root itself is a known XSD element (e.g.
|
||||
// CreateObjectDie), the root supplies the type context for its whole
|
||||
// subtree, so element/attribute validation is still reliable.
|
||||
const rootName = root ? localName(root.name) : "";
|
||||
const isFragment = rootName !== "AssetDeclaration";
|
||||
const validateTree =
|
||||
!isFragment || (root !== null && model.elementTypeName(rootName) !== null);
|
||||
|
||||
for (const el of doc.elements) {
|
||||
// Only report diagnostics for nodes that belong to the document being
|
||||
@@ -155,7 +166,7 @@ export class Ra3Diagnostics {
|
||||
const range = tagRange(document, el);
|
||||
|
||||
// Top-level assets must have an id.
|
||||
if (isTopLevel) {
|
||||
if (!isFragment && isTopLevel) {
|
||||
const idAttr = el.attrs.find((a) => a.name === "id");
|
||||
if (!idAttr || !idAttr.value) {
|
||||
diags.push(
|
||||
@@ -206,7 +217,7 @@ export class Ra3Diagnostics {
|
||||
const isXsdElement = model.isXsdElementName(el.name);
|
||||
|
||||
// Unknown element.
|
||||
if (settings.diagnoseUnknownElements && isXsdElement) {
|
||||
if (settings.diagnoseUnknownElements && isXsdElement && validateTree) {
|
||||
const knownType = model.elementTypeName(local);
|
||||
if (!knownType) {
|
||||
diags.push(
|
||||
@@ -221,7 +232,7 @@ export class Ra3Diagnostics {
|
||||
}
|
||||
|
||||
// Attributes.
|
||||
if (isXsdElement) {
|
||||
if (isXsdElement && validateTree) {
|
||||
const elType = resolveElementType(el);
|
||||
const knownAttrs = model.attributesOfType(elType);
|
||||
const knownNames = new Set(knownAttrs.map((a) => a.name));
|
||||
@@ -247,27 +258,70 @@ export class Ra3Diagnostics {
|
||||
}
|
||||
|
||||
if (!attr.hasValue) continue;
|
||||
this.checkValueReferences(
|
||||
elType,
|
||||
attr.name,
|
||||
attr.value,
|
||||
attr,
|
||||
document,
|
||||
idx,
|
||||
diags,
|
||||
provisional,
|
||||
);
|
||||
// References and $DEFINE constants inside fragments depend on the
|
||||
// includer's context; don't report them until P1 resolves the real
|
||||
// include sites.
|
||||
if (!isFragment) {
|
||||
this.checkValueReferences(
|
||||
elType,
|
||||
attr.name,
|
||||
attr.value,
|
||||
attr,
|
||||
document,
|
||||
idx,
|
||||
diags,
|
||||
provisional,
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!isFragment) {
|
||||
this.checkContentReferences(el, elType, document, idx, diags, provisional);
|
||||
}
|
||||
this.checkContentReferences(el, elType, document, idx, diags, provisional);
|
||||
}
|
||||
|
||||
// Include-specific checks.
|
||||
if (local === "Include") {
|
||||
this.checkInclude(el, document, idx, diags);
|
||||
} else if (local === "include" && el.name.toLowerCase().startsWith("xi:")) {
|
||||
this.checkXiInclude(el, document, idx, diags);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private checkXiInclude(
|
||||
el: XmlElement,
|
||||
document: vscode.TextDocument,
|
||||
idx: ModIndex | null,
|
||||
diags: vscode.Diagnostic[],
|
||||
): void {
|
||||
const hrefAttr = el.attrs.find((a) => a.name === "href");
|
||||
if (!hrefAttr?.hasValue) return;
|
||||
const searchPaths = idx
|
||||
? buildSearchPaths(idx.sdkDir, idx.projectDir)
|
||||
: this.ws.searchPaths(document);
|
||||
if (!searchPaths) return;
|
||||
const resolved = resolveSource(
|
||||
hrefAttr.value,
|
||||
dirname(document.uri.fsPath),
|
||||
searchPaths,
|
||||
);
|
||||
if (resolved.path) return;
|
||||
if (/^(DATA|ART|AUDIO):/i.test(hrefAttr.value.trim())) {
|
||||
if (this.sdkUnusable()) return;
|
||||
}
|
||||
diags.push(
|
||||
this.diag(
|
||||
new vscode.Range(
|
||||
document.positionAt(hrefAttr.valueStart),
|
||||
document.positionAt(hrefAttr.valueEnd),
|
||||
),
|
||||
t("xi:include target not found: {0}", hrefAttr.value),
|
||||
vscode.DiagnosticSeverity.Warning,
|
||||
"include-not-found",
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
private checkCrossFileDuplicate(
|
||||
type: string,
|
||||
id: string,
|
||||
|
||||
@@ -169,12 +169,26 @@ async function expandXi(
|
||||
if (!target?.parse?.root) return;
|
||||
|
||||
const xpointer = xi.attrs.find((a) => a.name === "xpointer")?.value ?? "";
|
||||
const selected = xpointer
|
||||
? findXPointerContainer(target.parse, xpointer)?.children ?? []
|
||||
: target.parse.root.children;
|
||||
|
||||
for (const sel of selected) {
|
||||
await handleChild(sel, logicalParent, resolved, depth + 1, ctx, elements, stack);
|
||||
if (xpointer) {
|
||||
const container = findXPointerContainer(target.parse, xpointer);
|
||||
if (!container) return;
|
||||
for (const sel of container.children) {
|
||||
await handleChild(sel, logicalParent, resolved, depth + 1, ctx, elements, stack);
|
||||
}
|
||||
} else {
|
||||
// XInclude semantics: without an xpointer the whole target document is
|
||||
// included, i.e. its root element replaces the <xi:include> node.
|
||||
// RA3 fragments such as GenericCelestialBuildingSuicide.xml rely on this
|
||||
// to splice the module element itself (CreateObjectDie) into the parent.
|
||||
await handleChild(
|
||||
target.parse.root,
|
||||
logicalParent,
|
||||
resolved,
|
||||
depth + 1,
|
||||
ctx,
|
||||
elements,
|
||||
stack,
|
||||
);
|
||||
}
|
||||
} finally {
|
||||
stack.delete(key);
|
||||
|
||||
@@ -452,3 +452,89 @@ test("diagnostics report unresolved typed content references only", async () =>
|
||||
"untyped WeakReference content is not diagnosed as a global ref",
|
||||
);
|
||||
});
|
||||
|
||||
test("fragment diagnostics skip document-level checks but keep subtree validation", async () => {
|
||||
const text =
|
||||
`<CreateObjectDie xmlns="uri:ea.com:eala:asset" id="ModuleTag_X" CreationList="OCL_X">\n` +
|
||||
` <DieMuxData DeathTypo="SUICIDED"/>\n` +
|
||||
`</CreateObjectDie>`;
|
||||
const scope = await makeScope(text, makeIdx([]));
|
||||
const collection = new FakeDiagnosticCollection();
|
||||
const provider = new Ra3Diagnostics({
|
||||
isRa3Workspace: () => true,
|
||||
getScope: async () => scope,
|
||||
settings: {
|
||||
diagnoseUnknownElements: true,
|
||||
reportUnresolvedReferences: "warning",
|
||||
},
|
||||
});
|
||||
provider["collection"] = collection;
|
||||
await provider.update(makeDocument(text));
|
||||
const codes = collection.last.diags.map((d) => d.code);
|
||||
assert.ok(
|
||||
!codes.includes("missing-id"),
|
||||
"fragment children are not treated as top-level assets",
|
||||
);
|
||||
assert.ok(
|
||||
!codes.some((c) => c.startsWith("unresolved-reference")),
|
||||
"fragment references are deferred to the includer context",
|
||||
);
|
||||
assert.ok(
|
||||
codes.includes("unknown-attribute"),
|
||||
"a known fragment root still validates its subtree attributes",
|
||||
);
|
||||
});
|
||||
|
||||
test("fragment diagnostics ignore unknown wrapper roots and still report missing xi:include", async () => {
|
||||
const text =
|
||||
`<CommonArmorDraws xmlns="uri:ea.com:eala:asset" xmlns:xi="http://www.w3.org/2001/XInclude">\n` +
|
||||
` <ScriptedModelDraw id="M" Bogus="x"/>\n` +
|
||||
` <xi:include href="MissingTarget.xml"/>\n` +
|
||||
`</CommonArmorDraws>`;
|
||||
const scope = await makeScope(text, makeIdx([]));
|
||||
const collection = new FakeDiagnosticCollection();
|
||||
const provider = new Ra3Diagnostics({
|
||||
isRa3Workspace: () => true,
|
||||
getScope: async () => scope,
|
||||
settings: {
|
||||
diagnoseUnknownElements: true,
|
||||
reportUnresolvedReferences: "warning",
|
||||
},
|
||||
});
|
||||
provider["collection"] = collection;
|
||||
await provider.update(makeDocument(text));
|
||||
const codes = collection.last.diags.map((d) => d.code);
|
||||
assert.ok(
|
||||
!codes.includes("unknown-element"),
|
||||
"wrapper roots are not validated as standalone documents",
|
||||
);
|
||||
assert.ok(
|
||||
!codes.includes("unknown-attribute"),
|
||||
"unknown wrapper roots do not trigger subtree attribute guessing",
|
||||
);
|
||||
assert.ok(
|
||||
codes.includes("include-not-found"),
|
||||
"missing xi:include targets inside fragments are still reported",
|
||||
);
|
||||
});
|
||||
|
||||
test("full documents still require ids on top-level assets", async () => {
|
||||
const text = `<AssetDeclaration>\n <GameObject/>\n</AssetDeclaration>`;
|
||||
const scope = await makeScope(text, makeIdx([]));
|
||||
const collection = new FakeDiagnosticCollection();
|
||||
const provider = new Ra3Diagnostics({
|
||||
isRa3Workspace: () => true,
|
||||
getScope: async () => scope,
|
||||
settings: {
|
||||
diagnoseUnknownElements: true,
|
||||
reportUnresolvedReferences: "warning",
|
||||
},
|
||||
});
|
||||
provider["collection"] = collection;
|
||||
await provider.update(makeDocument(text));
|
||||
const codes = collection.last.diags.map((d) => d.code);
|
||||
assert.ok(
|
||||
codes.includes("missing-id"),
|
||||
"AssetDeclaration documents keep top-level id checks",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -99,6 +99,54 @@ test("logical xi:include expansion gives included modules their Draws context",
|
||||
assert.ok(localIds.includes("ModuleTag_Headlight"));
|
||||
});
|
||||
|
||||
test("xi:include without xpointer splices the target root element itself", async (t) => {
|
||||
const tmp = await mkdtemp(join(tmpdir(), "ra3-local-noxpointer-"));
|
||||
t.after(() => rm(tmp, { recursive: true, force: true }));
|
||||
const dataDir = join(tmp, "Data");
|
||||
const includesDir = join(dataDir, "Includes");
|
||||
await mkdir(includesDir, { recursive: true });
|
||||
const mainPath = join(dataDir, "Main.xml");
|
||||
const fragmentPath = join(includesDir, "Fragment.xml");
|
||||
await writeFile(
|
||||
fragmentPath,
|
||||
'<CreateObjectDie xmlns="uri:ea.com:eala:asset" id="ModuleTag_X" CreationList="OCL_X"><DieMuxData DeathTypes="SUICIDED"/></CreateObjectDie>',
|
||||
"utf8",
|
||||
);
|
||||
await writeFile(
|
||||
mainPath,
|
||||
'<AssetDeclaration xmlns="uri:ea.com:eala:asset" xmlns:xi="http://www.w3.org/2001/XInclude">' +
|
||||
'<GameObject id="G"><Behaviors><xi:include href="DATA:Includes/Fragment.xml"/></Behaviors></GameObject>' +
|
||||
"</AssetDeclaration>",
|
||||
"utf8",
|
||||
);
|
||||
const searchPaths = buildSearchPaths(sdk, tmp);
|
||||
const text = await readFile(mainPath, "utf8");
|
||||
const scope = await buildDocumentScope(mainPath, text, 1, {
|
||||
projectDir: tmp,
|
||||
sdkDir: sdk,
|
||||
searchPaths,
|
||||
readRecords: readParsed,
|
||||
readDom: readParsed,
|
||||
});
|
||||
const behaviors = scope.expanded.elements.find((e) => e.name === "Behaviors");
|
||||
assert.ok(behaviors, "Behaviors exists");
|
||||
assert.equal(behaviors.children.length, 1);
|
||||
const module = behaviors.children[0];
|
||||
assert.equal(module.name, "CreateObjectDie");
|
||||
assert.equal(
|
||||
module.attrs.find((a) => a.name === "id")?.value,
|
||||
"ModuleTag_X",
|
||||
);
|
||||
assert.match(module.sourceFile, /Fragment\.xml$/i);
|
||||
const dieMux = scope.expanded.elements.find((e) => e.name === "DieMuxData");
|
||||
assert.ok(dieMux, "DieMuxData is expanded");
|
||||
assert.equal(
|
||||
dieMux.parent,
|
||||
module,
|
||||
"DieMuxData stays inside the included CreateObjectDie module",
|
||||
);
|
||||
});
|
||||
|
||||
test("local overlay wins over a global definition with the same id", async () => {
|
||||
const scope = await makeScope();
|
||||
const global = {
|
||||
|
||||
Reference in New Issue
Block a user