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。 + * + *

+ * スコープの仕組み: + *

+ * + * + *

+ * コンポーネントのレンダリング構造: + *

+ * + */ +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 = """ + + + dummy +
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 = "
slotB-default
"; + String compAMayaa = """ + + + + + + """; + // comp-b は rootVar を参照する末端コンポーネント + String compBHtml = "
dummy
"; + String compBMayaa = """ + + + + + + """; + // 3 段ネストを経由しても rootVar がプロトタイプチェーンで参照できる + String expected = """ + + +
from-root
+ + """; + + 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 = "
inner-default
"; + 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 = """ + + +
T+L1+L2
+ + """; + + 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 = "
inner-default
"; + // layout1 は l1Var を定義しない + String layout1Mayaa = """ + + + + + + """.formatted(casePath); + String layout2Html = """ + + +
outer-default
+ + """; + String layout2Mayaa = """ + + + + + + """; + String expected = """ + + +
T+NA+L2
+ + """; + + 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 = "
inner-default
"; + String layout1Mayaa = """ + + + + + + + """.formatted(casePath); + String layout2Html = """ + + +
outer-default
+ + """; + // layout2 は l2Var を定義しない + String layout2Mayaa = """ + + + + + """; + String expected = """ + + +
T+L1+NA
+ + """; + + 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 @@ + + + + +