diff --git a/src/test/java/org/seasar/mayaa/functional/engine/ScriptScopeIntegrationTest.java b/src/test/java/org/seasar/mayaa/functional/engine/ScriptScopeIntegrationTest.java
new file mode 100644
index 00000000..fdad2833
--- /dev/null
+++ b/src/test/java/org/seasar/mayaa/functional/engine/ScriptScopeIntegrationTest.java
@@ -0,0 +1,1324 @@
+/*
+ * Copyright 2004-2026 the Seasar Foundation and the Others.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+package org.seasar.mayaa.functional.engine;
+
+import java.io.IOException;
+import java.util.LinkedHashMap;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.seasar.mayaa.functional.EngineTestBase;
+import org.seasar.mayaa.impl.builder.TemplateBuilderImpl;
+import org.seasar.mayaa.impl.management.DiagnosticEventBuffer;
+import org.seasar.mayaa.impl.source.DynamicRegisteredSourceHolder;
+
+/**
+ * m:insert によるページ間でのスクリプトスコープの引き継ぎ動作を検証するIT。
+ *
+ *
+ * スコープの仕組み:
+ *
+ *
+ * - beforeRender はレンダリング前に {@code startScope} でスコープを積み、
+ * Rhino の PageAttributeScope プロトタイプチェーンに変数を登録する。
+ * - 各 renderTemplateProcessor 呼び出しでも {@code startScope/endScope} が対になって呼ばれ、
+ * これは常に現在のスコープの子スコープとして作成されるため、祖先スコープへのアクセスは
+ * プロトタイプチェーン経由で維持される。
+ * - insertProcessor が RenderUtil.renderPage を呼ぶ場合も同様に startScope を積むため、
+ * 呼び出し元の beforeRender 変数はコンポーネント内から参照できる(スコープ継承)。
+ * - 兄弟コンポーネントは共通祖先を共有するが、互いに子の関係になく独立したブランチに属するため、
+ * 一方のコンポーネントで宣言した変数は他方からは参照できない(スコープ分離)。
+ *
+ *
+ *
+ * コンポーネントのレンダリング構造:
+ *
+ *
+ * - コンポーネント HTML は {@code m:doRender id="root"} (replace=true デフォルト) を含む。
+ * replace=true の場合、ComponentRenderer はルート要素のタグを出力せず、
+ * その子プロセッサのみをレンダリングする。
+ * - m:insert id="slot" replace="false" により、スロット要素のタグは保持され、
+ * コンポーネントの出力がその内部に挿入される。
+ *
+ */
+public class ScriptScopeIntegrationTest extends EngineTestBase {
+
+ private static final String BASE = "/it-case/script-scope/";
+
+ /**
+ * ターゲットページの beforeRender で宣言した変数が、
+ * m:insert path で挿入されたコンポーネント内の m:write から参照できることを確認する。
+ *
+ *
+ * スコープチェーン(期待動作):
+ *
+ *
+ *
+ * global
+ * → beforeRenderScope (scriptVar ここに存在)
+ * → 各 renderTemplateProcessor が積む子スコープ
+ * → component の renderPage で積む scope
+ * → component の各 renderTemplateProcessor が積む子スコープ
+ * ※ m:write が ${scriptVar} を評価する際、プロトタイプチェーンを遡って
+ * beforeRenderScope の scriptVar を参照できる
+ *
+ */
+ @ParameterizedTest(name = "useNewParser {0}")
+ @ValueSource(booleans = { false, true })
+ public void beforeRenderで宣言した変数がm_insertコンポーネント内で参照できる(boolean useNewParser)
+ throws IOException {
+ setUseNewParser(useNewParser);
+ setAutoEscapeEnabled(false);
+
+ String caseName = "scope-propagate-to-insert";
+ String casePath = BASE + caseName + "/";
+ String targetHtmlPath = casePath + "target.html";
+ String targetMayaaPath = casePath + "target.mayaa";
+ String componentHtmlPath = casePath + "component.html";
+ String componentMayaaPath = casePath + "component.mayaa";
+ String expectedPath = casePath + "expected.html";
+
+ String targetHtml = """
+
+
+ slot-default
+
+ """;
+ String targetMayaa = """
+
+
+
+
+
+ """;
+ // m:doRender id="root" (replace=true デフォルト):
+ // - ComponentRenderer が insertRoot として DoRenderProcessor を使用
+ // - root 要素タグは出力されず、root の子要素のみがレンダリングされる
+ // m:write id="val" value="${scriptVar}":
+ // - val span 要素をスコープチェーンから取得した scriptVar の値で置き換える
+ String componentHtml = "dummy
";
+ String componentMayaa = """
+
+
+
+
+
+ """;
+ // beforeRender のスコープが m:insert コンポーネントに引き継がれるなら
+ // SlotDiv 内に "from-beforeRender" が出力される。
+ // スコープが引き継がれていない(バグ)場合は val span の出力が空になり、
+ // SlotDiv の内部も空になる。
+ String expected = """
+
+
+ from-beforeRender
+
+ """;
+
+ DynamicRegisteredSourceHolder.registerContents(targetHtmlPath, targetHtml);
+ DynamicRegisteredSourceHolder.registerContents(targetMayaaPath, targetMayaa);
+ DynamicRegisteredSourceHolder.registerContents(componentHtmlPath, componentHtml);
+ DynamicRegisteredSourceHolder.registerContents(componentMayaaPath, componentMayaa);
+ DynamicRegisteredSourceHolder.registerContents(expectedPath, expected);
+
+ execAndVerify(targetHtmlPath, expectedPath, new LinkedHashMap());
+ }
+
+ /**
+ * ターゲットページの beforeRender で宣言した変数が、
+ * 複数の兄弟 m:insert コンポーネントからそれぞれ参照できることを確認する。
+ *
+ *
+ * 兄弟コンポーネントは互いに独立したスコープブランチを持つが、
+ * 共通祖先スコープ(beforeRender で積まれたスコープ)の変数は
+ * プロトタイプチェーンをたどって両方から参照できる。
+ *
+ */
+ @ParameterizedTest(name = "useNewParser {0}")
+ @ValueSource(booleans = { false, true })
+ public void beforeRenderで宣言した変数が複数の兄弟m_insertコンポーネントからも参照できる(boolean useNewParser)
+ throws IOException {
+ setUseNewParser(useNewParser);
+ setAutoEscapeEnabled(false);
+
+ String caseName = "scope-propagate-to-sibling-inserts";
+ String casePath = BASE + caseName + "/";
+ String targetHtmlPath = casePath + "target.html";
+ String targetMayaaPath = casePath + "target.mayaa";
+ String componentHtmlPath = casePath + "component.html";
+ String componentMayaaPath = casePath + "component.mayaa";
+ String expectedPath = casePath + "expected.html";
+
+ String targetHtml = """
+
+
+ slot1-default
+ slot2-default
+
+ """;
+ String targetMayaa = """
+
+
+
+
+
+
+ """;
+ String componentHtml = "dummy
";
+ String componentMayaa = """
+
+
+
+
+
+ """;
+ // 兄弟の両コンポーネントがそれぞれ beforeRender の sharedVar を参照できる
+ String expected = """
+
+
+ shared-value
+ shared-value
+
+ """;
+
+ DynamicRegisteredSourceHolder.registerContents(targetHtmlPath, targetHtml);
+ DynamicRegisteredSourceHolder.registerContents(targetMayaaPath, targetMayaa);
+ DynamicRegisteredSourceHolder.registerContents(componentHtmlPath, componentHtml);
+ DynamicRegisteredSourceHolder.registerContents(componentMayaaPath, componentMayaa);
+ DynamicRegisteredSourceHolder.registerContents(expectedPath, expected);
+
+ execAndVerify(targetHtmlPath, expectedPath, new LinkedHashMap());
+ }
+
+ /**
+ * m:insert コンポーネント A の beforeRender で宣言した変数は、
+ * 同一ターゲットページに配置されたコンポーネント B からは参照できないことを確認する。
+ *
+ *
+ * コンポーネント A・B はそれぞれ独立した renderPage スコープブランチを持ち、
+ * A → B の順でレンダリングされる場合、A の endScope によって A のスコープは破棄される。
+ * B のレンダリング開始時、スコープチェーンのヒントは A の beforeRender 前の状態に戻っており、
+ * B の scope chain に compAVar は存在しない。
+ *
+ *
+ *
+ * JavaScript の {@code typeof} 演算子は未宣言変数に対して例外を投げず
+ * {@code "undefined"} 文字列を返すため、表明として安全に使用できる。
+ *
+ */
+ @ParameterizedTest(name = "useNewParser {0}")
+ @ValueSource(booleans = { false, true })
+ public void コンポーネントAのbeforeRenderで宣言した変数はコンポーネントBから参照できない(boolean useNewParser)
+ throws IOException {
+ setUseNewParser(useNewParser);
+ setAutoEscapeEnabled(false);
+
+ String caseName = "scope-isolation-between-sibling-inserts";
+ String casePath = BASE + caseName + "/";
+ String targetHtmlPath = casePath + "target.html";
+ String targetMayaaPath = casePath + "target.mayaa";
+ String compAHtmlPath = casePath + "comp-a.html";
+ String compAMayaaPath = casePath + "comp-a.mayaa";
+ String compBHtmlPath = casePath + "comp-b.html";
+ String compBMayaaPath = casePath + "comp-b.mayaa";
+ String expectedPath = casePath + "expected.html";
+
+ String targetHtml = """
+
+
+ slotA-default
+ slotB-default
+
+ """;
+ String targetMayaa = """
+
+
+
+
+
+ """;
+ // comp-a: beforeRender で privateVar を宣言して出力する
+ String compAHtml = "dummy
";
+ String compAMayaa = """
+
+
+
+
+
+
+ """;
+ // comp-b: comp-a の privateVar を参照しようとする。
+ // スコープが分離されているなら typeof privateVar は "undefined" を返し、
+ // 三項演算子によって "invisible" が出力される。
+ // スコープが誤って共有されている(バグ)なら "secret" が出力される。
+ String compBHtml = "dummy
";
+ String compBMayaa = """
+
+
+
+
+
+ """;
+ // comp-a は "secret"、comp-b はスコープ分離により "invisible" を出力する
+ String expected = """
+
+
+ secret
+ invisible
+
+ """;
+
+ DynamicRegisteredSourceHolder.registerContents(targetHtmlPath, targetHtml);
+ DynamicRegisteredSourceHolder.registerContents(targetMayaaPath, targetMayaa);
+ DynamicRegisteredSourceHolder.registerContents(compAHtmlPath, compAHtml);
+ DynamicRegisteredSourceHolder.registerContents(compAMayaaPath, compAMayaa);
+ DynamicRegisteredSourceHolder.registerContents(compBHtmlPath, compBHtml);
+ DynamicRegisteredSourceHolder.registerContents(compBMayaaPath, compBMayaa);
+ DynamicRegisteredSourceHolder.registerContents(expectedPath, expected);
+
+ execAndVerify(targetHtmlPath, expectedPath, new LinkedHashMap());
+ }
+
+ // ===================================================================
+ // m:extends レイアウト共有でのスコープ継承テスト
+ // ===================================================================
+
+ /**
+ * m:extends によるレイアウト共有において、
+ * レイアウトページの beforeRender で宣言した変数が
+ * コンテンツ(ターゲット)テンプレート内から参照できることを確認する。
+ *
+ *
+ * RenderUtil.renderPage の do-while ループは target → layout の順に実行し、
+ * 各ページの beforeRender で startScope を呼び出す。
+ *
+ *
+ *
+ * ループ1 (target) : startScope → S1 (targetVar がない場合はスコープのみ)
+ * ループ2 (layout) : startScope → S2 (layoutVar 登録), S2.parentScope = S1
+ * レンダリング時カレント = S2
+ *
+ *
+ * layout→target の順でテンプレートがスタックされ(LIFO)、layoutTemplate が先にレンダリングされる。
+ * layout の m:insert name="contentArea"(パスなし)は fireEvent=false で renderPage
+ * を呼ぶため
+ * startScope は追加されず、target テンプレートのプロセッサも S2 スコープで実行される。
+ * S2 に layoutVar が存在するため参照できる。
+ *
+ */
+ @ParameterizedTest(name = "useNewParser {0}")
+ @ValueSource(booleans = { false, true })
+ public void m_extendsレイアウトのbeforeRender変数がコンテンツテンプレートから参照できる(boolean useNewParser)
+ throws IOException {
+ setUseNewParser(useNewParser);
+ setAutoEscapeEnabled(false);
+
+ String caseName = "layout-extends-layout-var-to-content";
+ String casePath = BASE + caseName + "/";
+ String targetHtmlPath = casePath + "target.html";
+ String targetMayaaPath = casePath + "target.mayaa";
+ String layoutHtmlPath = casePath + "layout.html";
+ String layoutMayaaPath = casePath + "layout.mayaa";
+ String expectedPath = casePath + "expected.html";
+
+ // ターゲットHTML: コンテンツとして使われる div。内部の span に layoutVar を表示する
+ String targetHtml = "dummy
";
+ // ターゲットmayaa: m:extends でレイアウトを指定
+ // m:doRender id="content" → content div を contentArea という名前でコンテンツ登録
+ // m:write id="msg" → layoutVar を参照(layout の beforeRender で宣言)
+ String targetMayaa = """
+
+
+
+
+
+ """.formatted(casePath);
+ String layoutHtml = """
+
+
+ layout-default
+
+ """;
+ // レイアウト: beforeRender で layoutVar を宣言し、contentArea を挿入する
+ String layoutMayaa = """
+
+
+
+
+
+ """;
+ // content div (replace=true デフォルト) は子要素のみ出力される。
+ // m:write id="msg" → layoutVar = "from-layout" → span タグごと "from-layout"
+ // テキストに置換
+ // slot div 内に "from-layout" が展開される
+ String expected = """
+
+
+ from-layout
+
+ """;
+
+ DynamicRegisteredSourceHolder.registerContents(targetHtmlPath, targetHtml);
+ DynamicRegisteredSourceHolder.registerContents(targetMayaaPath, targetMayaa);
+ DynamicRegisteredSourceHolder.registerContents(layoutHtmlPath, layoutHtml);
+ DynamicRegisteredSourceHolder.registerContents(layoutMayaaPath, layoutMayaa);
+ DynamicRegisteredSourceHolder.registerContents(expectedPath, expected);
+
+ execAndVerify(targetHtmlPath, expectedPath, new LinkedHashMap());
+ }
+
+ /**
+ * m:extends によるレイアウト共有において、
+ * ターゲットページの beforeRender で宣言した変数が
+ * レイアウトテンプレート内から参照できることを確認する。
+ *
+ *
+ * スコープチェーン(m:extends の場合):
+ *
+ *
+ *
+ * ループ1 (target) : startScope → S1 (targetVar 登録)
+ * ループ2 (layout) : startScope → S2 (S2.parentScope = S1)
+ * レンダリング時カレント = S2
+ * layout テンプレートで ${targetVar} → S2 になし → S1 で発見 → "from-target"
+ *
+ */
+ @ParameterizedTest(name = "useNewParser {0}")
+ @ValueSource(booleans = { false, true })
+ public void m_extendsターゲットのbeforeRender変数がレイアウトテンプレートから参照できる(boolean useNewParser)
+ throws IOException {
+ setUseNewParser(useNewParser);
+ setAutoEscapeEnabled(false);
+
+ String caseName = "layout-extends-target-var-to-layout";
+ String casePath = BASE + caseName + "/";
+ String targetHtmlPath = casePath + "target.html";
+ String targetMayaaPath = casePath + "target.mayaa";
+ String layoutHtmlPath = casePath + "layout.html";
+ String layoutMayaaPath = casePath + "layout.mayaa";
+ String expectedPath = casePath + "expected.html";
+
+ String targetHtml = "PAGE CONTENT
";
+ // ターゲット: beforeRender で targetVar を宣言し、contentArea としてコンテンツを登録
+ String targetMayaa = """
+
+
+
+
+
+ """.formatted(casePath);
+ String layoutHtml = """
+
+
+ dummy
+ layout-default
+
+ """;
+ // レイアウト: msg に targetVar を出力し、slot に contentArea を挿入
+ String layoutMayaa = """
+
+
+
+
+
+ """;
+ // m:write id="msg" で span タグごと "from-target" に置換
+ // content div (replace=true) → 子テキスト "PAGE CONTENT" のみ出力
+ // slot div 内に "PAGE CONTENT" が展開
+ String expected = """
+
+
+ from-target
+ PAGE CONTENT
+
+ """;
+
+ DynamicRegisteredSourceHolder.registerContents(targetHtmlPath, targetHtml);
+ DynamicRegisteredSourceHolder.registerContents(targetMayaaPath, targetMayaa);
+ DynamicRegisteredSourceHolder.registerContents(layoutHtmlPath, layoutHtml);
+ DynamicRegisteredSourceHolder.registerContents(layoutMayaaPath, layoutMayaa);
+ DynamicRegisteredSourceHolder.registerContents(expectedPath, expected);
+
+ execAndVerify(targetHtmlPath, expectedPath, new LinkedHashMap());
+ }
+
+ /**
+ * beforeRender 内で load 関数を使って別の JavaScript ファイルを読み込むと、
+ * そのファイルで定義された変数・関数がカレントスコープ(beforeRender スコープ)に展開され、
+ * 同ページのテンプレートから参照できることを確認する。
+ *
+ *
+ * 仕組み:
+ *
+ *
+ * - {@code ServiceCycle.load(systemID)} は
+ * {@code SourceCompiledScriptImpl.execute()} を呼び出す。
+ * - {@code execute()} 内では {@code RhinoUtil.getScope()}(カレント
+ * PageAttributeScope)で実行される。
+ * - よって lib.js の変数宣言や関数定義は beforeRender のスコープに直接登録される。
+ * - スクリプトから {@code load("path")} と直接呼べるのは、{@code ServiceCycle} が Rhino の
+ * スコープ親として {@code wrapAsJavaObject} でラップされているため。
+ *
+ *
+ *
+ * m:extends レイアウト共有と組み合わせたケース: ターゲットの beforeRender で lib.js を load し、
+ * layout テンプレート・content テンプレート双方から lib.js の変数を参照できることも確認する。
+ *
+ */
+ @ParameterizedTest(name = "useNewParser {0}")
+ @ValueSource(booleans = { false, true })
+ public void m_extendsのbeforeRender内でload関数で読み込んだJSが同スコープに展開される(boolean useNewParser)
+ throws IOException {
+ setUseNewParser(useNewParser);
+ setAutoEscapeEnabled(false);
+
+ String caseName = "layout-extends-with-load";
+ String casePath = BASE + caseName + "/";
+ String targetHtmlPath = casePath + "target.html";
+ String targetMayaaPath = casePath + "target.mayaa";
+ String layoutHtmlPath = casePath + "layout.html";
+ String layoutMayaaPath = casePath + "layout.mayaa";
+ String libJsPath = casePath + "lib.js";
+ String expectedPath = casePath + "expected.html";
+
+ // lib.js: 変数と関数を定義する(load 後に同スコープで参照できることを確認)
+ String libJs = """
+ var libMessage = "from-lib";
+ function libHelper() { return "helper-result"; }
+ """;
+ // target HTML: コンテンツエリア。lib.js の変数と関数を使った出力用 span を含む
+ String targetHtml = """
+
+ dummy
+ dummy
+
+ """;
+ // target mayaa: beforeRender で lib.js を load し、layoutも load した変数を使える
+ String targetMayaa = """
+
+
+
+
+
+
+
+ """.formatted(casePath, libJsPath);
+ String layoutHtml = """
+
+
+
+ layout-default
+
+ """;
+ // layout mayaa: target の beforeRender でセットした libMsgCopy を header で出力
+ // これにより target の beforeRender→load が layout テンプレートからも参照できることを確認
+ String layoutMayaa = """
+
+
+
+
+
+ """;
+ // header span: libMsgCopy = "from-lib"(target の beforeRender で lib.js load
+ // 後にコピー)
+ // content (replace=true) の中:
+ // libMsg span: libMessage = "from-lib"(lib.js の変数)
+ // libFunc span: libHelper() = "helper-result"(lib.js の関数)
+ String expected = """
+
+
+ from-lib
+
+ from-lib
+ helper-result
+
+
+ """;
+
+ DynamicRegisteredSourceHolder.registerContents(libJsPath, libJs);
+ DynamicRegisteredSourceHolder.registerContents(targetHtmlPath, targetHtml);
+ DynamicRegisteredSourceHolder.registerContents(targetMayaaPath, targetMayaa);
+ DynamicRegisteredSourceHolder.registerContents(layoutHtmlPath, layoutHtml);
+ DynamicRegisteredSourceHolder.registerContents(layoutMayaaPath, layoutMayaa);
+ DynamicRegisteredSourceHolder.registerContents(expectedPath, expected);
+
+ execAndVerify(targetHtmlPath, expectedPath, new LinkedHashMap());
+ }
+
+ // ===================================================================
+ // 変数シャドウイングテスト
+ // ===================================================================
+
+ /**
+ * target と layout の両方で同名変数を beforeRender で宣言した場合、
+ * より後に積まれる layout の beforeRender スコープ(S2)の値が優先されることを確認する。
+ *
+ *
+ * スコープチェーン:
+ *
+ *
+ *
+ * S1 (target beforeRender) : myVar = "from-target"
+ * S2 (layout beforeRender) : myVar = "from-layout", S2.parentScope = S1
+ * レンダリング時カレント = S2 → ${myVar} は S2 で発見 → "from-layout" が優先
+ *
+ */
+ @ParameterizedTest(name = "useNewParser {0}")
+ @ValueSource(booleans = { false, true })
+ public void targetとlayoutで同名変数を定義した場合layoutの値が優先される(boolean useNewParser)
+ throws IOException {
+ setUseNewParser(useNewParser);
+ setAutoEscapeEnabled(false);
+
+ String caseName = "scope-shadow-target-vs-layout";
+ String casePath = BASE + caseName + "/";
+ String targetHtmlPath = casePath + "target.html";
+ String targetMayaaPath = casePath + "target.mayaa";
+ String layoutHtmlPath = casePath + "layout.html";
+ String layoutMayaaPath = casePath + "layout.mayaa";
+ String expectedPath = casePath + "expected.html";
+
+ String targetHtml = "dummy
";
+ // target: 同名変数 myVar を "from-target" で宣言
+ String targetMayaa = """
+
+
+
+
+
+
+ """.formatted(casePath);
+ String layoutHtml = """
+
+
+ layout-default
+
+ """;
+ // layout: 同名変数 myVar を "from-layout" で宣言
+ // layout の S2 は target の S1 の子であり、S2 での myVar が優先される
+ String layoutMayaa = """
+
+
+
+
+
+ """;
+ // content div 内の msg span: layout の S2 を先に参照するため "from-layout"
+ String expected = """
+
+
+ from-layout
+
+ """;
+
+ DynamicRegisteredSourceHolder.registerContents(targetHtmlPath, targetHtml);
+ DynamicRegisteredSourceHolder.registerContents(targetMayaaPath, targetMayaa);
+ DynamicRegisteredSourceHolder.registerContents(layoutHtmlPath, layoutHtml);
+ DynamicRegisteredSourceHolder.registerContents(layoutMayaaPath, layoutMayaa);
+ DynamicRegisteredSourceHolder.registerContents(expectedPath, expected);
+
+ execAndVerify(targetHtmlPath, expectedPath, new LinkedHashMap());
+ }
+
+ // ===================================================================
+ // 深いネストでのスコープ継承テスト
+ // ===================================================================
+
+ /**
+ * target → comp-a → comp-b の 3 段ネストにおいて、
+ * target の beforeRender で宣言した変数が末端の comp-b からも参照できることを確認する。
+ *
+ *
+ * スコープチェーン:
+ *
+ *
+ *
+ * S0 (target beforeRender) : rootVar = "from-root"
+ * → S1 (comp-a renderPage scope)
+ * → S2 (comp-b renderPage scope)
+ * ${rootVar} → S2 → S1 → S0 で発見 → "from-root"
+ *
+ */
+ @ParameterizedTest(name = "useNewParser {0}")
+ @ValueSource(booleans = { false, true })
+ public void targetのbeforeRender変数が3段ネストコンポーネントから参照できる(boolean useNewParser)
+ throws IOException {
+ setUseNewParser(useNewParser);
+ setAutoEscapeEnabled(false);
+
+ String caseName = "scope-propagate-3-level-nest";
+ String casePath = BASE + caseName + "/";
+ String targetHtmlPath = casePath + "target.html";
+ String targetMayaaPath = casePath + "target.mayaa";
+ String compAHtmlPath = casePath + "comp-a.html";
+ String compAMayaaPath = casePath + "comp-a.mayaa";
+ String compBHtmlPath = casePath + "comp-b.html";
+ String compBMayaaPath = casePath + "comp-b.mayaa";
+ String expectedPath = casePath + "expected.html";
+
+ String targetHtml = """
+
+
+ slotA-default
+
+ """;
+ String targetMayaa = """
+
+
+
+
+
+ """;
+ // comp-a は slotB を持ち、そこに comp-b を挿入する中継コンポーネント
+ String compAHtml = "";
+ String compAMayaa = """
+
+
+
+
+
+ """;
+ // comp-b は rootVar を参照する末端コンポーネント
+ String compBHtml = "dummy
";
+ String compBMayaa = """
+
+
+
+
+
+ """;
+ // 3 段ネストを経由しても rootVar がプロトタイプチェーンで参照できる
+ String expected = """
+
+
+
+
+ """;
+
+ DynamicRegisteredSourceHolder.registerContents(targetHtmlPath, targetHtml);
+ DynamicRegisteredSourceHolder.registerContents(targetMayaaPath, targetMayaa);
+ DynamicRegisteredSourceHolder.registerContents(compAHtmlPath, compAHtml);
+ DynamicRegisteredSourceHolder.registerContents(compAMayaaPath, compAMayaa);
+ DynamicRegisteredSourceHolder.registerContents(compBHtmlPath, compBHtml);
+ DynamicRegisteredSourceHolder.registerContents(compBMayaaPath, compBMayaa);
+ DynamicRegisteredSourceHolder.registerContents(expectedPath, expected);
+
+ execAndVerify(targetHtmlPath, expectedPath, new LinkedHashMap());
+ }
+
+ // ===================================================================
+ // 祖先スコープ変数の変更(Mutation)テスト
+ // ===================================================================
+
+ /**
+ * コンポーネント A が {@code var} なしで祖先スコープの変数を書き換えた場合、
+ * 兄弟コンポーネント B にもその変更が見えることを確認する。
+ *
+ *
+ * JavaScript のスコープチェーンにおいて {@code var} なしの代入は
+ * プロトタイプチェーンを遡って変数が宣言されたスコープを書き換える({@code ScriptRuntime.setName})。
+ * comp-a が target の S0 の {@code sharedVar} を書き換えた後、
+ * comp-b も S0 を共有しているため変更後の値を参照する。
+ *
+ *
+ *
+ * S0 (target beforeRender) : sharedVar = "original"
+ * comp-a beforeRender : sharedVar = "mutated-by-a" ← var なし → S0 を書き換え
+ * comp-b : ${sharedVar} → S0 → "mutated-by-a" (変更が見える)
+ *
+ */
+ @ParameterizedTest(name = "useNewParser {0}")
+ @ValueSource(booleans = { false, true })
+ public void コンポーネントAがvar無しで祖先変数を書き換えると兄弟Bにも変更が見える(boolean useNewParser)
+ throws IOException {
+ setUseNewParser(useNewParser);
+ setAutoEscapeEnabled(false);
+
+ String caseName = "scope-mutation-by-component";
+ String casePath = BASE + caseName + "/";
+ String targetHtmlPath = casePath + "target.html";
+ String targetMayaaPath = casePath + "target.mayaa";
+ String compAHtmlPath = casePath + "comp-a.html";
+ String compAMayaaPath = casePath + "comp-a.mayaa";
+ String compBHtmlPath = casePath + "comp-b.html";
+ String compBMayaaPath = casePath + "comp-b.mayaa";
+ String expectedPath = casePath + "expected.html";
+
+ String targetHtml = """
+
+
+ slotA-default
+ slotB-default
+
+ """;
+ String targetMayaa = """
+
+
+
+
+
+
+ """;
+ // comp-a: var なしで祖先スコープ(S0)の sharedVar を書き換える
+ String compAHtml = "dummy
";
+ String compAMayaa = """
+
+
+
+
+
+
+ """;
+ // comp-b: sharedVar を参照する。S0 が書き換えられているため "mutated-by-a" が見える
+ String compBHtml = "dummy
";
+ String compBMayaa = """
+
+
+
+
+
+ """;
+ // comp-a は "mutated-by-a"、comp-b も同じ祖先スコープ経由で "mutated-by-a" を参照する
+ String expected = """
+
+
+ mutated-by-a
+ mutated-by-a
+
+ """;
+
+ DynamicRegisteredSourceHolder.registerContents(targetHtmlPath, targetHtml);
+ DynamicRegisteredSourceHolder.registerContents(targetMayaaPath, targetMayaa);
+ DynamicRegisteredSourceHolder.registerContents(compAHtmlPath, compAHtml);
+ DynamicRegisteredSourceHolder.registerContents(compAMayaaPath, compAMayaa);
+ DynamicRegisteredSourceHolder.registerContents(compBHtmlPath, compBHtml);
+ DynamicRegisteredSourceHolder.registerContents(compBMayaaPath, compBMayaa);
+ DynamicRegisteredSourceHolder.registerContents(expectedPath, expected);
+
+ execAndVerify(targetHtmlPath, expectedPath, new LinkedHashMap());
+ }
+
+ // ===================================================================
+ // m:insert インフォーマルプロパティテスト
+ // ===================================================================
+
+ /**
+ * m:insert に追加したカスタム属性(インフォーマルプロパティ)が、
+ * コンポーネント内で {@code binding} スコープ経由で参照できることを確認する。
+ *
+ *
+ * InsertProcessor はインフォーマルプロパティを収集して BindingScope に渡す。
+ * コンポーネント内では {@code ${binding.propName}} で値を取得できる。
+ *
+ */
+ @ParameterizedTest(name = "useNewParser {0}")
+ @ValueSource(booleans = { false, true })
+ public void m_insertのインフォーマルプロパティがbindingスコープで参照できる(boolean useNewParser)
+ throws IOException {
+ setUseNewParser(useNewParser);
+ setAutoEscapeEnabled(false);
+
+ String caseName = "scope-insert-informal-property";
+ String casePath = BASE + caseName + "/";
+ String targetHtmlPath = casePath + "target.html";
+ String targetMayaaPath = casePath + "target.mayaa";
+ String componentHtmlPath = casePath + "component.html";
+ String componentMayaaPath = casePath + "component.mayaa";
+ String expectedPath = casePath + "expected.html";
+
+ String targetHtml = """
+
+
+ slot-default
+
+ """;
+ // m:insert に myParam="hello-binding" を追加(インフォーマルプロパティ)
+ String targetMayaa = """
+
+
+
+
+ """;
+ String componentHtml = "dummy
";
+ // コンポーネント内で binding.myParam としてインフォーマルプロパティを参照する
+ String componentMayaa = """
+
+
+
+
+
+ """;
+ String expected = """
+
+
+ hello-binding
+
+ """;
+
+ DynamicRegisteredSourceHolder.registerContents(targetHtmlPath, targetHtml);
+ DynamicRegisteredSourceHolder.registerContents(targetMayaaPath, targetMayaa);
+ DynamicRegisteredSourceHolder.registerContents(componentHtmlPath, componentHtml);
+ DynamicRegisteredSourceHolder.registerContents(componentMayaaPath, componentMayaa);
+ DynamicRegisteredSourceHolder.registerContents(expectedPath, expected);
+
+ execAndVerify(targetHtmlPath, expectedPath, new LinkedHashMap());
+ }
+
+ /**
+ * m:insert のインフォーマルプロパティで未定義キーを参照した場合、
+ * 例外を起こさずフォールバック値を使えることを確認する。
+ *
+ *
+ * 既存キーの参照だけでなく、未定義キー参照時の安全な扱いを検証することで
+ * binding スコープ利用時の境界条件をカバーする。
+ *
+ */
+ @ParameterizedTest(name = "useNewParser {0}")
+ @ValueSource(booleans = { false, true })
+ public void m_insertの未定義インフォーマルプロパティ参照でフォールバックできる(boolean useNewParser)
+ throws IOException {
+ setUseNewParser(useNewParser);
+ setAutoEscapeEnabled(false);
+
+ String caseName = "scope-insert-informal-property-missing";
+ String casePath = BASE + caseName + "/";
+ String targetHtmlPath = casePath + "target.html";
+ String targetMayaaPath = casePath + "target.mayaa";
+ String componentHtmlPath = casePath + "component.html";
+ String componentMayaaPath = casePath + "component.mayaa";
+ String expectedPath = casePath + "expected.html";
+
+ String targetHtml = """
+
+
+ slot-default
+
+ """;
+ // myParam は渡すが、コンポーネント側では notProvided を参照してフォールバックを確認する
+ String targetMayaa = """
+
+
+
+
+ """;
+ String componentHtml = "dummy
";
+ // 未定義キー binding.notProvided は null/undefined 相当として扱い、missing を返す
+ String componentMayaa = """
+
+
+
+
+
+ """;
+ String expected = """
+
+
+ missing
+
+ """;
+
+ DynamicRegisteredSourceHolder.registerContents(targetHtmlPath, targetHtml);
+ DynamicRegisteredSourceHolder.registerContents(targetMayaaPath, targetMayaa);
+ DynamicRegisteredSourceHolder.registerContents(componentHtmlPath, componentHtml);
+ DynamicRegisteredSourceHolder.registerContents(componentMayaaPath, componentMayaa);
+ DynamicRegisteredSourceHolder.registerContents(expectedPath, expected);
+
+ execAndVerify(targetHtmlPath, expectedPath, new LinkedHashMap());
+ }
+
+ // ===================================================================
+ // 多段 m:extends(3 段)のスコープテスト
+ // ===================================================================
+
+ /**
+ * target → layout1 → layout2 の 3 段 m:extends チェーンにおいて、
+ * すべての階層の beforeRender 変数がターゲットテンプレートのプロセッサから参照できることを確認する。
+ *
+ *
+ * スコープチェーン:
+ *
+ *
+ *
+ * S1 (target beforeRender) : tVar = "T"
+ * S2 (layout1 beforeRender) : l1Var = "L1", S2.parentScope = S1
+ * S3 (layout2 beforeRender) : l2Var = "L2", S3.parentScope = S2
+ * レンダリング時カレント = S3
+ * ${tVar} → S3 → S2 → S1 → "T"
+ * ${l1Var} → S3 → S2 → "L1"
+ * ${l2Var} → S3 → "L2"
+ *
+ */
+ @ParameterizedTest(name = "useNewParser {0}")
+ @ValueSource(booleans = { false, true })
+ public void _3段m_extendsチェーンで全階層のbeforeRender変数が参照できる(boolean useNewParser) throws IOException {
+ setUseNewParser(useNewParser);
+ setAutoEscapeEnabled(false);
+
+ String caseName = "layout-extends-3level-chain";
+ String casePath = BASE + caseName + "/";
+ String targetHtmlPath = casePath + "target.html";
+ String targetMayaaPath = casePath + "target.mayaa";
+ String layout1HtmlPath = casePath + "layout1.html";
+ String layout1MayaaPath = casePath + "layout1.mayaa";
+ String layout2HtmlPath = casePath + "layout2.html";
+ String layout2MayaaPath = casePath + "layout2.mayaa";
+ String expectedPath = casePath + "expected.html";
+
+ // target: 全 3 階層の変数を連結して出力する span を含むコンテンツ
+ String targetHtml = "dummy
";
+ String targetMayaa = """
+
+
+
+
+
+
+ """.formatted(casePath);
+ // layout1: layout2 を継承し、contentArea を中継する
+ String layout1Html = "";
+ String layout1Mayaa = """
+
+
+
+
+
+
+ """.formatted(casePath);
+ // layout2: 最外殻 HTML。outerContent スロットに layout1 のコンテンツを挿入する
+ String layout2Html = """
+
+
+ outer-default
+
+ """;
+ String layout2Mayaa = """
+
+
+
+
+
+ """;
+ // outerSlot → layout1 の l1Root 子要素 → innerSlot → target の tContent 子要素 →
+ // "T+L1+L2"
+ String expected = """
+
+
+
+
+ """;
+
+ DynamicRegisteredSourceHolder.registerContents(targetHtmlPath, targetHtml);
+ DynamicRegisteredSourceHolder.registerContents(targetMayaaPath, targetMayaa);
+ DynamicRegisteredSourceHolder.registerContents(layout1HtmlPath, layout1Html);
+ DynamicRegisteredSourceHolder.registerContents(layout1MayaaPath, layout1Mayaa);
+ DynamicRegisteredSourceHolder.registerContents(layout2HtmlPath, layout2Html);
+ DynamicRegisteredSourceHolder.registerContents(layout2MayaaPath, layout2Mayaa);
+ DynamicRegisteredSourceHolder.registerContents(expectedPath, expected);
+
+ execAndVerify(targetHtmlPath, expectedPath, new LinkedHashMap());
+ }
+
+ /**
+ * 3 段 m:extends チェーンで layout1 の beforeRender 変数が未定義でも、
+ * target/layout2 の変数は参照でき、未定義変数だけフォールバックできることを確認する。
+ */
+ @ParameterizedTest(name = "useNewParser {0}")
+ @ValueSource(booleans = { false, true })
+ public void _3段m_extendsチェーンで中間階層変数が未定義でも他階層は参照できる(boolean useNewParser)
+ throws IOException {
+ setUseNewParser(useNewParser);
+ setAutoEscapeEnabled(false);
+
+ String caseName = "layout-extends-3level-chain-missing-l1";
+ String casePath = BASE + caseName + "/";
+ String targetHtmlPath = casePath + "target.html";
+ String targetMayaaPath = casePath + "target.mayaa";
+ String layout1HtmlPath = casePath + "layout1.html";
+ String layout1MayaaPath = casePath + "layout1.mayaa";
+ String layout2HtmlPath = casePath + "layout2.html";
+ String layout2MayaaPath = casePath + "layout2.mayaa";
+ String expectedPath = casePath + "expected.html";
+
+ String targetHtml = "dummy
";
+ String targetMayaa = """
+
+
+
+
+
+
+ """.formatted(casePath);
+ String layout1Html = "";
+ // layout1 は l1Var を定義しない
+ String layout1Mayaa = """
+
+
+
+
+
+ """.formatted(casePath);
+ String layout2Html = """
+
+
+ outer-default
+
+ """;
+ String layout2Mayaa = """
+
+
+
+
+
+ """;
+ String expected = """
+
+
+
+
+ """;
+
+ DynamicRegisteredSourceHolder.registerContents(targetHtmlPath, targetHtml);
+ DynamicRegisteredSourceHolder.registerContents(targetMayaaPath, targetMayaa);
+ DynamicRegisteredSourceHolder.registerContents(layout1HtmlPath, layout1Html);
+ DynamicRegisteredSourceHolder.registerContents(layout1MayaaPath, layout1Mayaa);
+ DynamicRegisteredSourceHolder.registerContents(layout2HtmlPath, layout2Html);
+ DynamicRegisteredSourceHolder.registerContents(layout2MayaaPath, layout2Mayaa);
+ DynamicRegisteredSourceHolder.registerContents(expectedPath, expected);
+
+ execAndVerify(targetHtmlPath, expectedPath, new LinkedHashMap());
+ }
+
+ /**
+ * 3 段 m:extends チェーンで layout2 の beforeRender 変数が未定義でも、
+ * target/layout1 の変数は参照でき、未定義変数だけフォールバックできることを確認する。
+ */
+ @ParameterizedTest(name = "useNewParser {0}")
+ @ValueSource(booleans = { false, true })
+ public void _3段m_extendsチェーンで最上位階層変数が未定義でも他階層は参照できる(boolean useNewParser)
+ throws IOException {
+ setUseNewParser(useNewParser);
+ setAutoEscapeEnabled(false);
+
+ String caseName = "layout-extends-3level-chain-missing-l2";
+ String casePath = BASE + caseName + "/";
+ String targetHtmlPath = casePath + "target.html";
+ String targetMayaaPath = casePath + "target.mayaa";
+ String layout1HtmlPath = casePath + "layout1.html";
+ String layout1MayaaPath = casePath + "layout1.mayaa";
+ String layout2HtmlPath = casePath + "layout2.html";
+ String layout2MayaaPath = casePath + "layout2.mayaa";
+ String expectedPath = casePath + "expected.html";
+
+ String targetHtml = "dummy
";
+ String targetMayaa = """
+
+
+
+
+
+
+ """.formatted(casePath);
+ String layout1Html = "";
+ String layout1Mayaa = """
+
+
+
+
+
+
+ """.formatted(casePath);
+ String layout2Html = """
+
+
+ outer-default
+
+ """;
+ // layout2 は l2Var を定義しない
+ String layout2Mayaa = """
+
+
+
+
+ """;
+ String expected = """
+
+
+
+
+ """;
+
+ DynamicRegisteredSourceHolder.registerContents(targetHtmlPath, targetHtml);
+ DynamicRegisteredSourceHolder.registerContents(targetMayaaPath, targetMayaa);
+ DynamicRegisteredSourceHolder.registerContents(layout1HtmlPath, layout1Html);
+ DynamicRegisteredSourceHolder.registerContents(layout1MayaaPath, layout1Mayaa);
+ DynamicRegisteredSourceHolder.registerContents(layout2HtmlPath, layout2Html);
+ DynamicRegisteredSourceHolder.registerContents(layout2MayaaPath, layout2Mayaa);
+ DynamicRegisteredSourceHolder.registerContents(expectedPath, expected);
+
+ execAndVerify(targetHtmlPath, expectedPath, new LinkedHashMap());
+ }
+
+ // ===================================================================
+ // コンポーネントのローカルスコープ分離テスト
+ // ===================================================================
+
+ /**
+ * コンポーネントの beforeRender で {@code var} を使って同名変数を宣言した場合、
+ * 祖先スコープの同名変数をシャドウする(局所的に上書き)が、
+ * コンポーネント終了後は祖先スコープの変数が元の値を保持していることを確認する。
+ *
+ *
+ * {@code var} による宣言はコンポーネントのローカルスコープに新しい束縛を作成するため、
+ * 祖先スコープ(target の S0)の {@code myVar} は変更されない。
+ *
+ *
+ *
+ * S0 (target beforeRender) : myVar = "from-target"
+ * S2 (comp beforeRender) : myVar = "from-component" (var → S2 に新規束縛)
+ * comp 内 ${myVar} → S2 で発見 → "from-component"
+ * comp 終了後 target の ${myVar} → S0 で発見 → "from-target" (不変)
+ *
+ */
+ @ParameterizedTest(name = "useNewParser {0}")
+ @ValueSource(booleans = { false, true })
+ public void コンポーネントがvar宣言で同名変数をシャドウしても親スコープは不変(boolean useNewParser)
+ throws IOException {
+ setUseNewParser(useNewParser);
+ setAutoEscapeEnabled(false);
+
+ String caseName = "scope-component-local-shadow";
+ String casePath = BASE + caseName + "/";
+ String targetHtmlPath = casePath + "target.html";
+ String targetMayaaPath = casePath + "target.mayaa";
+ String componentHtmlPath = casePath + "component.html";
+ String componentMayaaPath = casePath + "component.mayaa";
+ String expectedPath = casePath + "expected.html";
+
+ String targetHtml = """
+
+
+ slot-default
+ dummy
+
+ """;
+ // target: beforeRender で myVar = "from-target" にセット
+ // slot にコンポーネントを挿入し、その後 targetVal で target 側の myVar を出力する
+ String targetMayaa = """
+
+
+
+
+
+
+ """;
+ String componentHtml = "dummy
";
+ // component: var でローカルスコープに myVar = "from-component" を宣言
+ // → 祖先スコープの myVar は変更されない
+ String componentMayaa = """
+
+
+
+
+
+
+ """;
+ // slot: コンポーネントは "from-component" を出力(ローカルスコープから参照)
+ // targetVal: target の myVar は "from-target" のまま(コンポーネントのシャドウは局所的)
+ String expected = """
+
+
+ from-component
+ from-target
+
+ """;
+
+ DynamicRegisteredSourceHolder.registerContents(targetHtmlPath, targetHtml);
+ DynamicRegisteredSourceHolder.registerContents(targetMayaaPath, targetMayaa);
+ DynamicRegisteredSourceHolder.registerContents(componentHtmlPath, componentHtml);
+ DynamicRegisteredSourceHolder.registerContents(componentMayaaPath, componentMayaa);
+ DynamicRegisteredSourceHolder.registerContents(expectedPath, expected);
+
+ execAndVerify(targetHtmlPath, expectedPath, new LinkedHashMap());
+ }
+
+ void setUseNewParser(boolean useNewParser) {
+ getServiceProvider().getTemplateBuilder().setParameter(
+ TemplateBuilderImpl.USE_NEW_PARSER, Boolean.toString(useNewParser));
+ }
+
+ void setAutoEscapeEnabled(boolean enabled) {
+ getServiceProvider().getScriptEnvironment().setParameter(
+ "autoEscapeEnabled", Boolean.toString(enabled));
+ }
+
+ @AfterEach
+ void cleanup() {
+ DynamicRegisteredSourceHolder.unregisterAll();
+ DiagnosticEventBuffer.clear();
+ }
+}
diff --git a/src/test/java/org/seasar/mayaa/functional/layout/LayoutBeforeRenderIntegrationTest.java b/src/test/java/org/seasar/mayaa/functional/layout/LayoutBeforeRenderIntegrationTest.java
new file mode 100644
index 00000000..7827fb08
--- /dev/null
+++ b/src/test/java/org/seasar/mayaa/functional/layout/LayoutBeforeRenderIntegrationTest.java
@@ -0,0 +1,380 @@
+/*
+ * Copyright 2004-2026 the Seasar Foundation and the Others.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND,
+ * either express or implied. See the License for the specific language
+ * governing permissions and limitations under the License.
+ */
+package org.seasar.mayaa.functional.layout;
+
+import java.io.IOException;
+import java.util.LinkedHashMap;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.ValueSource;
+import org.seasar.mayaa.functional.EngineTestBase;
+import org.seasar.mayaa.impl.builder.TemplateBuilderImpl;
+import org.seasar.mayaa.impl.management.DiagnosticEventBuffer;
+import org.seasar.mayaa.impl.source.DynamicRegisteredSourceHolder;
+
+/**
+ * レイアウト共有ページにおける beforeRender スクリプトの動作を検証するIT。
+ */
+public class LayoutBeforeRenderIntegrationTest extends EngineTestBase {
+
+ private static final String BASE = "/it-case/layout-integration/";
+
+ /**
+ * レイアウトページの mayaa に定義した beforeRender スクリプトが
+ * レンダリング前に実行され、スクリプト内で設定した変数が
+ * レイアウトテンプレート内の m:write から参照できることを確認する。
+ */
+ @ParameterizedTest(name = "useNewParser {0}")
+ @ValueSource(booleans = { false, true })
+ public void レイアウトページのbeforeRenderスクリプトが実行される(boolean useNewParser)
+ throws IOException {
+ setUseNewParser(useNewParser);
+ setAutoEscapeEnabled(false);
+ enableDump();
+
+ String caseName = "layout-before-render-basic";
+ String casePath = BASE + caseName + "/";
+ String targetHtmlPath = casePath + "target.html";
+ String targetMayaaPath = casePath + "target.mayaa";
+ String layoutHtmlPath = casePath + "layout.html";
+ String layoutMayaaPath = casePath + "layout.mayaa";
+ String expectedPath = casePath + "expected.html";
+
+ String targetHtml = "PAGE
";
+ String targetMayaa = """
+
+
+
+
+ """;
+ String layoutHtml = """
+
+
+ dummy
+ layout-default
+
+ """;
+ String layoutMayaa = """
+
+
+
+
+
+
+ """;
+ String expected = """
+
+
+ from-layout-beforeRender
+ PAGE
+
+ """;
+
+ DynamicRegisteredSourceHolder.registerContents(targetHtmlPath, targetHtml);
+ DynamicRegisteredSourceHolder.registerContents(targetMayaaPath, targetMayaa);
+ DynamicRegisteredSourceHolder.registerContents(layoutHtmlPath, layoutHtml);
+ DynamicRegisteredSourceHolder.registerContents(layoutMayaaPath, layoutMayaa);
+ DynamicRegisteredSourceHolder.registerContents(expectedPath, expected);
+
+ execAndVerify(targetHtmlPath, expectedPath, new LinkedHashMap());
+ }
+
+ /**
+ * ターゲットページの mayaa と レイアウトページの mayaa の両方に
+ * beforeRender スクリプトが定義されている場合に、どちらも実行されることを確認する。
+ * ターゲットの beforeRender で設定した変数はレイアウトの m:write から参照できる。
+ */
+ @ParameterizedTest(name = "useNewParser {0}")
+ @ValueSource(booleans = { false, true })
+ public void ターゲットとレイアウト両方のbeforeRenderが実行される(boolean useNewParser)
+ throws IOException {
+ setUseNewParser(useNewParser);
+ setAutoEscapeEnabled(false);
+ enableDump();
+
+ String caseName = "layout-and-target-before-render";
+ String casePath = BASE + caseName + "/";
+ String targetHtmlPath = casePath + "target.html";
+ String targetMayaaPath = casePath + "target.mayaa";
+ String layoutHtmlPath = casePath + "layout.html";
+ String layoutMayaaPath = casePath + "layout.mayaa";
+ String expectedPath = casePath + "expected.html";
+
+ String targetHtml = "PAGE
";
+ String targetMayaa = """
+
+
+
+
+
+ """;
+ String layoutHtml = """
+
+
+ dummy
+ dummy
+ layout-default
+
+ """;
+ String layoutMayaa = """
+
+
+
+
+
+
+
+ """;
+ String expected = """
+
+
+ from-layout
+ from-target
+ PAGE
+
+ """;
+
+ DynamicRegisteredSourceHolder.registerContents(targetHtmlPath, targetHtml);
+ DynamicRegisteredSourceHolder.registerContents(targetMayaaPath, targetMayaa);
+ DynamicRegisteredSourceHolder.registerContents(layoutHtmlPath, layoutHtml);
+ DynamicRegisteredSourceHolder.registerContents(layoutMayaaPath, layoutMayaa);
+ DynamicRegisteredSourceHolder.registerContents(expectedPath, expected);
+
+ execAndVerify(targetHtmlPath, expectedPath, new LinkedHashMap());
+ }
+
+ /**
+ * target/layout の beforeRender 実行順序が target → layout であることを確認する。
+ */
+ @ParameterizedTest(name = "useNewParser {0}")
+ @ValueSource(booleans = { false, true })
+ public void beforeRenderの実行順序がtarget先layout後である(boolean useNewParser)
+ throws IOException {
+ setUseNewParser(useNewParser);
+ setAutoEscapeEnabled(false);
+ enableDump();
+
+ String caseName = "layout-before-render-order";
+ String casePath = BASE + caseName + "/";
+ String targetHtmlPath = casePath + "target.html";
+ String targetMayaaPath = casePath + "target.mayaa";
+ String layoutHtmlPath = casePath + "layout.html";
+ String layoutMayaaPath = casePath + "layout.mayaa";
+ String expectedPath = casePath + "expected.html";
+
+ String targetHtml = "PAGE
";
+ String targetMayaa = """
+
+
+
+
+
+ """;
+ String layoutHtml = """
+
+
+ dummy
+ layout-default
+
+ """;
+ String layoutMayaa = """
+
+
+
+
+
+
+ """;
+ String expected = """
+
+
+ TL
+ PAGE
+
+ """;
+
+ DynamicRegisteredSourceHolder.registerContents(targetHtmlPath, targetHtml);
+ DynamicRegisteredSourceHolder.registerContents(targetMayaaPath, targetMayaa);
+ DynamicRegisteredSourceHolder.registerContents(layoutHtmlPath, layoutHtml);
+ DynamicRegisteredSourceHolder.registerContents(layoutMayaaPath, layoutMayaa);
+ DynamicRegisteredSourceHolder.registerContents(expectedPath, expected);
+
+ execAndVerify(targetHtmlPath, expectedPath, new LinkedHashMap());
+ }
+
+ /**
+ * target/layout が同じ request キーへ代入した場合、
+ * 後から実行される layout 側の値で上書きされることを確認する。
+ */
+ @ParameterizedTest(name = "useNewParser {0}")
+ @ValueSource(booleans = { false, true })
+ public void targetとlayoutで同名requestキーに代入した場合layoutが優先される(boolean useNewParser)
+ throws IOException {
+ setUseNewParser(useNewParser);
+ setAutoEscapeEnabled(false);
+ enableDump();
+
+ String caseName = "layout-before-render-request-shadow";
+ String casePath = BASE + caseName + "/";
+ String targetHtmlPath = casePath + "target.html";
+ String targetMayaaPath = casePath + "target.mayaa";
+ String layoutHtmlPath = casePath + "layout.html";
+ String layoutMayaaPath = casePath + "layout.mayaa";
+ String expectedPath = casePath + "expected.html";
+
+ String targetHtml = "PAGE
";
+ String targetMayaa = """
+
+
+
+
+
+ """;
+ String layoutHtml = """
+
+
+ dummy
+ layout-default
+
+ """;
+ String layoutMayaa = """
+
+
+
+
+
+
+ """;
+ String expected = """
+
+
+ from-layout
+ PAGE
+
+ """;
+
+ DynamicRegisteredSourceHolder.registerContents(targetHtmlPath, targetHtml);
+ DynamicRegisteredSourceHolder.registerContents(targetMayaaPath, targetMayaa);
+ DynamicRegisteredSourceHolder.registerContents(layoutHtmlPath, layoutHtml);
+ DynamicRegisteredSourceHolder.registerContents(layoutMayaaPath, layoutMayaa);
+ DynamicRegisteredSourceHolder.registerContents(expectedPath, expected);
+
+ execAndVerify(targetHtmlPath, expectedPath, new LinkedHashMap());
+ }
+
+ /**
+ * request 以外のスコープ(target の var 変数)も、
+ * レイアウトテンプレートから参照できることを確認する。
+ */
+ @ParameterizedTest(name = "useNewParser {0}")
+ @ValueSource(booleans = { false, true })
+ public void targetのvar変数をlayoutから参照できる(boolean useNewParser)
+ throws IOException {
+ setUseNewParser(useNewParser);
+ setAutoEscapeEnabled(false);
+ enableDump();
+
+ String caseName = "layout-before-render-non-request-scope";
+ String casePath = BASE + caseName + "/";
+ String targetHtmlPath = casePath + "target.html";
+ String targetMayaaPath = casePath + "target.mayaa";
+ String layoutHtmlPath = casePath + "layout.html";
+ String layoutMayaaPath = casePath + "layout.mayaa";
+ String expectedPath = casePath + "expected.html";
+
+ String targetHtml = "PAGE
";
+ String targetMayaa = """
+
+
+
+
+
+ """;
+ String layoutHtml = """
+
+
+ dummy
+ layout-default
+
+ """;
+ String layoutMayaa = """
+
+
+
+
+
+ """;
+ String expected = """
+
+
+ from-target-local
+ PAGE
+
+ """;
+
+ DynamicRegisteredSourceHolder.registerContents(targetHtmlPath, targetHtml);
+ DynamicRegisteredSourceHolder.registerContents(targetMayaaPath, targetMayaa);
+ DynamicRegisteredSourceHolder.registerContents(layoutHtmlPath, layoutHtml);
+ DynamicRegisteredSourceHolder.registerContents(layoutMayaaPath, layoutMayaa);
+ DynamicRegisteredSourceHolder.registerContents(expectedPath, expected);
+
+ execAndVerify(targetHtmlPath, expectedPath, new LinkedHashMap());
+ }
+
+ void setUseNewParser(boolean useNewParser) {
+ getServiceProvider().getTemplateBuilder().setParameter(
+ TemplateBuilderImpl.USE_NEW_PARSER, Boolean.toString(useNewParser));
+ }
+
+ void setAutoEscapeEnabled(boolean enabled) {
+ getServiceProvider().getScriptEnvironment().setParameter(
+ "autoEscapeEnabled", Boolean.toString(enabled));
+ }
+
+ @AfterEach
+ void cleanup() {
+ DynamicRegisteredSourceHolder.unregisterAll();
+ DiagnosticEventBuffer.clear();
+ }
+}
diff --git a/src/test/java/org/seasar/mayaa/functional/layout/WEB-INF/org.seasar.mayaa.source.PageSourceFactory b/src/test/java/org/seasar/mayaa/functional/layout/WEB-INF/org.seasar.mayaa.source.PageSourceFactory
new file mode 100644
index 00000000..62a6d5fe
--- /dev/null
+++ b/src/test/java/org/seasar/mayaa/functional/layout/WEB-INF/org.seasar.mayaa.source.PageSourceFactory
@@ -0,0 +1,8 @@
+
+
+
+
+