diff --git a/src/pages-service/tickets/components/ProductDetail.vue b/src/pages-service/tickets/components/ProductDetail.vue
index 6a05c3e..09fd3ac 100644
--- a/src/pages-service/tickets/components/ProductDetail.vue
+++ b/src/pages-service/tickets/components/ProductDetail.vue
@@ -1,6 +1,26 @@
-
-
+
+
+
+
+
+
+
+
+
+
@@ -62,10 +97,37 @@
diff --git a/src/pages-service/tickets/view-contract.test.mjs b/src/pages-service/tickets/view-contract.test.mjs
index c2d741f..5cb4ae6 100644
--- a/src/pages-service/tickets/view-contract.test.mjs
+++ b/src/pages-service/tickets/view-contract.test.mjs
@@ -192,6 +192,19 @@ test("TicketHome prefers mediaList and falls back to coverImage", async () => {
assert.doesNotMatch(source, / {
+ const tabs = await readTicketFile("components/TicketDetailTabs.vue");
+
+ assert.match(tabs, / {
const indexPage = await readTicketFile("index.vue");
const detailPage = await readTicketFile("product-detail.vue");
@@ -214,7 +227,27 @@ test("product detail is an independent prototype-aligned page", async () => {
assert.match(detailPage, /=\s*scrollHeight - 4/
+ );
assert.match(component, /relative h-56 overflow-hidden bg-white/);
assert.doesNotMatch(component, /1 \/ \{\{ productImages\.length \}\}/);
assert.doesNotMatch(component, /productImages\.slice/);
@@ -235,7 +268,7 @@ test("product detail is an independent prototype-aligned page", async () => {
]) {
assert.ok(component.includes(field), `${field} is not rendered`);
}
- assert.match(component, /v-for="section in detailSections"/);
+ assert.match(component, /v-for="\(section, index\) in detailSections"/);
assert.match(component, /section\.sectionCode/);
assert.match(component, /section\.title/);
assert.match(component, /section\.content/);
@@ -268,6 +301,44 @@ test("product detail is an independent prototype-aligned page", async () => {
);
});
+test("product detail tab clicks win until the visitor resumes manual scrolling", async () => {
+ const component = await readTicketFile("components/ProductDetail.vue");
+ const factorySource = component.match(
+ /const createDetailSectionScrollSelection = \(\) => \{[\s\S]*?^\};?\n/m
+ )?.[0];
+
+ assert.ok(factorySource, "inline detail section scroll selection factory missing");
+ assert.match(component, /const detailSectionScrollSelection =\s*createDetailSectionScrollSelection\(\)/);
+ assert.match(
+ component,
+ /const selectDetailSection = \(sectionCode\) => \{\s*activeSectionCode\.value = detailSectionScrollSelection\.select/
+ );
+ assert.match(
+ component,
+ /const enableDetailScrollSync = \(\) => \{\s*detailSectionScrollSelection\.resumeScrollSync\(\)/
+ );
+ assert.match(
+ component,
+ /activeSectionCode\.value = detailSectionScrollSelection\.resolve\(/
+ );
+
+ const createDetailSectionScrollSelection = Function(
+ `${factorySource}\nreturn createDetailSectionScrollSelection`
+ )();
+ const selection = createDetailSectionScrollSelection();
+ const clickedSectionCode = selection.select("PURCHASE_NOTICE");
+
+ assert.equal(
+ selection.resolve(clickedSectionCode, "PRODUCT_DETAIL"),
+ "PURCHASE_NOTICE"
+ );
+ selection.resumeScrollSync();
+ assert.equal(
+ selection.resolve(clickedSectionCode, "FEE_INCLUDE"),
+ "FEE_INCLUDE"
+ );
+});
+
test("booking flow matches the visitor prototype while preserving requested controls", async () => {
const component = await readTicketFile("components/BookingFlow.vue");
const page = await readTicketFile("booking.vue");