feat: 创建商品详情的交互
This commit is contained in:
@@ -1,6 +1,26 @@
|
|||||||
<template>
|
<template>
|
||||||
<view class="flex h-full min-w-0 flex-col overflow-hidden bg-slate-100">
|
<view class="relative flex h-full min-w-0 flex-col overflow-hidden bg-slate-100">
|
||||||
<scroll-view class="min-h-0 flex-1" scroll-y>
|
<view
|
||||||
|
v-if="isTabsPinned && detailSections.length"
|
||||||
|
class="absolute left-0 right-0 top-0 z-30 border-b border-slate-100 bg-white shadow-[0_4px_12px_rgba(15,23,42,0.08)]"
|
||||||
|
>
|
||||||
|
<TicketDetailTabs
|
||||||
|
:sections="detailSections"
|
||||||
|
:active-section-code="activeSectionCode"
|
||||||
|
instance-key="pinned"
|
||||||
|
@select="selectDetailSection"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<scroll-view
|
||||||
|
id="ticket-detail-scroll"
|
||||||
|
class="min-h-0 flex-1"
|
||||||
|
scroll-y
|
||||||
|
:scroll-top="requestedScrollTop"
|
||||||
|
scroll-with-animation
|
||||||
|
@scroll="handleDetailScroll"
|
||||||
|
@touchstart="enableDetailScrollSync"
|
||||||
|
>
|
||||||
<view v-if="productImages.length" class="relative h-56 overflow-hidden bg-white">
|
<view v-if="productImages.length" class="relative h-56 overflow-hidden bg-white">
|
||||||
<TicketMediaSwiper
|
<TicketMediaSwiper
|
||||||
:media-list="productImages"
|
:media-list="productImages"
|
||||||
@@ -29,7 +49,21 @@
|
|||||||
</view>
|
</view>
|
||||||
|
|
||||||
<view
|
<view
|
||||||
v-for="section in detailSections"
|
v-if="detailSections.length"
|
||||||
|
id="ticket-detail-tabs-anchor"
|
||||||
|
class="mt-[12px] border-y border-slate-100 bg-white"
|
||||||
|
>
|
||||||
|
<TicketDetailTabs
|
||||||
|
:sections="detailSections"
|
||||||
|
:active-section-code="activeSectionCode"
|
||||||
|
instance-key="inline"
|
||||||
|
@select="selectDetailSection"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<view
|
||||||
|
v-for="(section, index) in detailSections"
|
||||||
|
:id="`ticket-detail-section-${index}`"
|
||||||
:key="section.sectionCode"
|
:key="section.sectionCode"
|
||||||
class="mx-[12px] mt-[12px] rounded-[16px] bg-white p-[16px]"
|
class="mx-[12px] mt-[12px] rounded-[16px] bg-white p-[16px]"
|
||||||
:class="section === detailSections[detailSections.length - 1] ? 'mb-[20px]' : ''"
|
:class="section === detailSections[detailSections.length - 1] ? 'mb-[20px]' : ''"
|
||||||
@@ -42,6 +76,7 @@
|
|||||||
:show-img-menu="false"
|
:show-img-menu="false"
|
||||||
:set-title="false"
|
:set-title="false"
|
||||||
:tag-style="detailContentTagStyle"
|
:tag-style="detailContentTagStyle"
|
||||||
|
@ready="measureDetailSectionOffsets"
|
||||||
/>
|
/>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
@@ -62,10 +97,37 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { computed } from "vue";
|
import {
|
||||||
|
computed,
|
||||||
|
getCurrentInstance,
|
||||||
|
nextTick,
|
||||||
|
onMounted,
|
||||||
|
ref,
|
||||||
|
watch,
|
||||||
|
} from "vue";
|
||||||
import MpHtml from "@/uni_modules/zero-markdown-view/components/mp-html/mp-html.vue";
|
import MpHtml from "@/uni_modules/zero-markdown-view/components/mp-html/mp-html.vue";
|
||||||
|
import TicketDetailTabs from "./TicketDetailTabs.vue";
|
||||||
import TicketMediaSwiper from "./TicketMediaSwiper.vue";
|
import TicketMediaSwiper from "./TicketMediaSwiper.vue";
|
||||||
|
|
||||||
|
const createDetailSectionScrollSelection = () => {
|
||||||
|
let shouldSyncFromScroll = true;
|
||||||
|
|
||||||
|
return {
|
||||||
|
select(sectionCode) {
|
||||||
|
shouldSyncFromScroll = false;
|
||||||
|
return String(sectionCode ?? "");
|
||||||
|
},
|
||||||
|
resumeScrollSync() {
|
||||||
|
shouldSyncFromScroll = true;
|
||||||
|
},
|
||||||
|
resolve(currentSectionCode, scrollSectionCode) {
|
||||||
|
return shouldSyncFromScroll
|
||||||
|
? String(scrollSectionCode ?? "")
|
||||||
|
: String(currentSectionCode ?? "");
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
product: { type: Object, required: true },
|
product: { type: Object, required: true },
|
||||||
});
|
});
|
||||||
@@ -96,6 +158,181 @@ const detailSections = computed(() =>
|
|||||||
(left, right) => Number(left.sortOrder || 0) - Number(right.sortOrder || 0)
|
(left, right) => Number(left.sortOrder || 0) - Number(right.sortOrder || 0)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
const activeSectionCode = ref(
|
||||||
|
String(detailSections.value[0]?.sectionCode ?? "")
|
||||||
|
);
|
||||||
|
const requestedScrollTop = ref(0);
|
||||||
|
const currentDetailScrollTop = ref(0);
|
||||||
|
const detailScrollHeight = ref(0);
|
||||||
|
const detailScrollViewportHeight = ref(0);
|
||||||
|
const tabsOffsetTop = ref(Number.POSITIVE_INFINITY);
|
||||||
|
const tabsHeight = ref(48);
|
||||||
|
const detailSectionOffsets = ref([]);
|
||||||
|
const isTabsPinned = ref(false);
|
||||||
|
const pendingSectionCode = ref("");
|
||||||
|
const detailSectionScrollSelection =
|
||||||
|
createDetailSectionScrollSelection();
|
||||||
|
const instance = getCurrentInstance();
|
||||||
|
|
||||||
|
const scrollToMeasuredDetailSection = (sectionCode) => {
|
||||||
|
const normalizedSectionCode = String(sectionCode ?? "");
|
||||||
|
const sectionOffset = detailSectionOffsets.value.find(
|
||||||
|
(section) => section.code === normalizedSectionCode
|
||||||
|
);
|
||||||
|
if (!sectionOffset) return false;
|
||||||
|
|
||||||
|
const targetScrollTop = Math.max(
|
||||||
|
0,
|
||||||
|
sectionOffset.top - tabsHeight.value
|
||||||
|
);
|
||||||
|
requestedScrollTop.value = currentDetailScrollTop.value;
|
||||||
|
nextTick(() => {
|
||||||
|
requestedScrollTop.value = targetScrollTop;
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectDetailSection = (sectionCode) => {
|
||||||
|
activeSectionCode.value = detailSectionScrollSelection.select(sectionCode);
|
||||||
|
if (scrollToMeasuredDetailSection(activeSectionCode.value)) return;
|
||||||
|
|
||||||
|
pendingSectionCode.value = activeSectionCode.value;
|
||||||
|
measureDetailSectionOffsets();
|
||||||
|
};
|
||||||
|
|
||||||
|
const enableDetailScrollSync = () => {
|
||||||
|
detailSectionScrollSelection.resumeScrollSync();
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolveScrollSectionCode = (scrollTop, scrollHeight) => {
|
||||||
|
if (!detailSectionOffsets.value.length) return "";
|
||||||
|
|
||||||
|
const isAtBottom =
|
||||||
|
scrollHeight > 0 &&
|
||||||
|
detailScrollViewportHeight.value > 0 &&
|
||||||
|
scrollTop + detailScrollViewportHeight.value >=
|
||||||
|
scrollHeight - 4;
|
||||||
|
if (isAtBottom) {
|
||||||
|
return detailSectionOffsets.value[detailSectionOffsets.value.length - 1]
|
||||||
|
.code;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activationLine =
|
||||||
|
scrollTop + (isTabsPinned.value ? tabsHeight.value + 8 : 8);
|
||||||
|
return detailSectionOffsets.value.reduce(
|
||||||
|
(current, section) =>
|
||||||
|
section.top <= activationLine ? section : current,
|
||||||
|
detailSectionOffsets.value[0]
|
||||||
|
).code;
|
||||||
|
};
|
||||||
|
|
||||||
|
const syncActiveDetailSection = (scrollTop, scrollHeight) => {
|
||||||
|
const scrollSectionCode = resolveScrollSectionCode(
|
||||||
|
scrollTop,
|
||||||
|
scrollHeight
|
||||||
|
);
|
||||||
|
if (!scrollSectionCode) return;
|
||||||
|
|
||||||
|
activeSectionCode.value = detailSectionScrollSelection.resolve(
|
||||||
|
activeSectionCode.value,
|
||||||
|
scrollSectionCode
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const measureDetailSectionOffsets = () => {
|
||||||
|
if (!detailSections.value.length) {
|
||||||
|
tabsOffsetTop.value = Number.POSITIVE_INFINITY;
|
||||||
|
detailSectionOffsets.value = [];
|
||||||
|
isTabsPinned.value = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
nextTick(() => {
|
||||||
|
const query = uni.createSelectorQuery().in(instance?.proxy);
|
||||||
|
query.select("#ticket-detail-scroll").boundingClientRect();
|
||||||
|
query.select("#ticket-detail-tabs-anchor").boundingClientRect();
|
||||||
|
detailSections.value.forEach((section, index) => {
|
||||||
|
query
|
||||||
|
.select(`#ticket-detail-section-${index}`)
|
||||||
|
.boundingClientRect();
|
||||||
|
});
|
||||||
|
query.exec((rects = []) => {
|
||||||
|
const scrollRect = rects[0];
|
||||||
|
const tabsRect = rects[1];
|
||||||
|
if (!scrollRect || !tabsRect) return;
|
||||||
|
|
||||||
|
const scrollTop = currentDetailScrollTop.value;
|
||||||
|
detailScrollViewportHeight.value = Number(scrollRect.height || 0);
|
||||||
|
tabsOffsetTop.value = Math.max(
|
||||||
|
0,
|
||||||
|
Number(tabsRect.top || 0) - Number(scrollRect.top || 0) + scrollTop
|
||||||
|
);
|
||||||
|
tabsHeight.value = Number(tabsRect.height || 48);
|
||||||
|
detailSectionOffsets.value = detailSections.value
|
||||||
|
.map((section, index) => {
|
||||||
|
const sectionRect = rects[index + 2];
|
||||||
|
if (!sectionRect) return null;
|
||||||
|
return {
|
||||||
|
code: String(section.sectionCode ?? ""),
|
||||||
|
top: Math.max(
|
||||||
|
0,
|
||||||
|
Number(sectionRect.top || 0) -
|
||||||
|
Number(scrollRect.top || 0) +
|
||||||
|
scrollTop
|
||||||
|
),
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(Boolean)
|
||||||
|
.sort((left, right) => left.top - right.top);
|
||||||
|
isTabsPinned.value = scrollTop >= tabsOffsetTop.value;
|
||||||
|
syncActiveDetailSection(scrollTop, detailScrollHeight.value);
|
||||||
|
|
||||||
|
if (pendingSectionCode.value) {
|
||||||
|
const sectionCode = pendingSectionCode.value;
|
||||||
|
pendingSectionCode.value = "";
|
||||||
|
scrollToMeasuredDetailSection(sectionCode);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDetailScroll = (event) => {
|
||||||
|
const scrollTop = Number(event.detail?.scrollTop || 0);
|
||||||
|
const scrollHeight = Number(event.detail?.scrollHeight || 0);
|
||||||
|
currentDetailScrollTop.value = scrollTop;
|
||||||
|
detailScrollHeight.value = scrollHeight;
|
||||||
|
isTabsPinned.value = scrollTop >= tabsOffsetTop.value;
|
||||||
|
|
||||||
|
if (!detailSectionOffsets.value.length) {
|
||||||
|
measureDetailSectionOffsets();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
syncActiveDetailSection(scrollTop, scrollHeight);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(measureDetailSectionOffsets);
|
||||||
|
watch(
|
||||||
|
() =>
|
||||||
|
detailSections.value
|
||||||
|
.map((section) => String(section.sectionCode ?? ""))
|
||||||
|
.join(","),
|
||||||
|
() => {
|
||||||
|
const activeExists = detailSections.value.some(
|
||||||
|
(section) =>
|
||||||
|
String(section.sectionCode ?? "") === activeSectionCode.value
|
||||||
|
);
|
||||||
|
if (!activeExists) {
|
||||||
|
activeSectionCode.value = String(
|
||||||
|
detailSections.value[0]?.sectionCode ?? ""
|
||||||
|
);
|
||||||
|
}
|
||||||
|
detailSectionScrollSelection.resumeScrollSync();
|
||||||
|
detailSectionOffsets.value = [];
|
||||||
|
tabsOffsetTop.value = Number.POSITIVE_INFINITY;
|
||||||
|
isTabsPinned.value = false;
|
||||||
|
measureDetailSectionOffsets();
|
||||||
|
}
|
||||||
|
);
|
||||||
const currencySymbol = computed(() =>
|
const currencySymbol = computed(() =>
|
||||||
props.product.currency === "CNY" ? "¥" : props.product.currency || ""
|
props.product.currency === "CNY" ? "¥" : props.product.currency || ""
|
||||||
);
|
);
|
||||||
|
|||||||
54
src/pages-service/tickets/components/TicketDetailTabs.vue
Normal file
54
src/pages-service/tickets/components/TicketDetailTabs.vue
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<template>
|
||||||
|
<scroll-view
|
||||||
|
class="h-[48px] w-full whitespace-nowrap bg-white"
|
||||||
|
scroll-x
|
||||||
|
:scroll-into-view="activeTabId"
|
||||||
|
scroll-with-animation
|
||||||
|
>
|
||||||
|
<view class="inline-flex h-[48px] min-w-full items-center px-[8px]">
|
||||||
|
<view
|
||||||
|
v-for="(section, index) in sections"
|
||||||
|
:id="`ticket-detail-tab-${instanceKey}-${index}`"
|
||||||
|
:key="`${section.sectionCode}-${index}`"
|
||||||
|
class="relative inline-flex h-[48px] flex-none items-center justify-center px-[14px]"
|
||||||
|
@click="emit('select', section.sectionCode)"
|
||||||
|
>
|
||||||
|
<text
|
||||||
|
class="text-[14px] leading-[20px]"
|
||||||
|
:class="isActiveSection(section.sectionCode) ? 'font-semibold text-[#f26b32]' : 'font-normal text-slate-500'"
|
||||||
|
>
|
||||||
|
{{ section.title }}
|
||||||
|
</text>
|
||||||
|
<view
|
||||||
|
v-if="isActiveSection(section.sectionCode)"
|
||||||
|
class="absolute bottom-0 left-1/2 h-[3px] w-[20px] -translate-x-1/2 rounded-full bg-[#f26b32]"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
</scroll-view>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
sections: { type: Array, default: () => [] },
|
||||||
|
activeSectionCode: { type: [String, Number], default: "" },
|
||||||
|
instanceKey: { type: String, default: "default" },
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(["select"]);
|
||||||
|
const activeIndex = computed(() =>
|
||||||
|
props.sections.findIndex(
|
||||||
|
(section) =>
|
||||||
|
String(section.sectionCode) === String(props.activeSectionCode)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
const activeTabId = computed(() =>
|
||||||
|
activeIndex.value >= 0
|
||||||
|
? `ticket-detail-tab-${props.instanceKey}-${activeIndex.value}`
|
||||||
|
: ""
|
||||||
|
);
|
||||||
|
const isActiveSection = (sectionCode) =>
|
||||||
|
String(sectionCode) === String(props.activeSectionCode);
|
||||||
|
</script>
|
||||||
@@ -192,6 +192,19 @@ test("TicketHome prefers mediaList and falls back to coverImage", async () => {
|
|||||||
assert.doesNotMatch(source, /<image\s+v-else-if="homeData\.coverImage"/);
|
assert.doesNotMatch(source, /<image\s+v-else-if="homeData\.coverImage"/);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("ticket detail section tabs scroll horizontally and keep the active tab visible", async () => {
|
||||||
|
const tabs = await readTicketFile("components/TicketDetailTabs.vue");
|
||||||
|
|
||||||
|
assert.match(tabs, /<scroll-view/);
|
||||||
|
assert.match(tabs, /scroll-x/);
|
||||||
|
assert.match(tabs, /:scroll-into-view="activeTabId"/);
|
||||||
|
assert.match(tabs, /scroll-with-animation/);
|
||||||
|
assert.match(tabs, /v-for="\(section, index\) in sections"/);
|
||||||
|
assert.match(tabs, /section\.title/);
|
||||||
|
assert.match(tabs, /emit\('select', section\.sectionCode\)/);
|
||||||
|
assert.match(tabs, /isActiveSection\(section\.sectionCode\)/);
|
||||||
|
});
|
||||||
|
|
||||||
test("product detail is an independent prototype-aligned page", async () => {
|
test("product detail is an independent prototype-aligned page", async () => {
|
||||||
const indexPage = await readTicketFile("index.vue");
|
const indexPage = await readTicketFile("index.vue");
|
||||||
const detailPage = await readTicketFile("product-detail.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, /<ProductDetail\s+v-else-if="product"/);
|
assert.match(detailPage, /<ProductDetail\s+v-else-if="product"/);
|
||||||
assert.match(detailPage, /\/pages-service\/tickets\/booking\?productId=/);
|
assert.match(detailPage, /\/pages-service\/tickets\/booking\?productId=/);
|
||||||
assert.match(component, /import TicketMediaSwiper from "\.\/TicketMediaSwiper\.vue"/);
|
assert.match(component, /import TicketMediaSwiper from "\.\/TicketMediaSwiper\.vue"/);
|
||||||
|
assert.match(component, /import TicketDetailTabs from "\.\/TicketDetailTabs\.vue"/);
|
||||||
assert.match(component, /<TicketMediaSwiper\s+:media-list="productImages"/);
|
assert.match(component, /<TicketMediaSwiper\s+:media-list="productImages"/);
|
||||||
|
assert.equal((component.match(/<TicketDetailTabs/g) || []).length, 2);
|
||||||
|
assert.match(component, /instance-key="inline"/);
|
||||||
|
assert.match(component, /instance-key="pinned"/);
|
||||||
|
assert.match(component, /v-if="isTabsPinned && detailSections\.length"/);
|
||||||
|
assert.match(component, /id="ticket-detail-tabs-anchor"/);
|
||||||
|
assert.match(component, /id="ticket-detail-scroll"/);
|
||||||
|
assert.match(component, /:scroll-top="requestedScrollTop"/);
|
||||||
|
assert.match(component, /@scroll="handleDetailScroll"/);
|
||||||
|
assert.match(component, /@touchstart="enableDetailScrollSync"/);
|
||||||
|
assert.match(component, /:id="`ticket-detail-section-\$\{index\}`"/);
|
||||||
|
assert.match(component, /@ready="measureDetailSectionOffsets"/);
|
||||||
|
assert.match(
|
||||||
|
component,
|
||||||
|
/uni\.createSelectorQuery\(\)\.in\(instance\?\.proxy\)/
|
||||||
|
);
|
||||||
|
assert.match(
|
||||||
|
component,
|
||||||
|
/scrollTop \+ detailScrollViewportHeight\.value >=\s*scrollHeight - 4/
|
||||||
|
);
|
||||||
assert.match(component, /relative h-56 overflow-hidden bg-white/);
|
assert.match(component, /relative h-56 overflow-hidden bg-white/);
|
||||||
assert.doesNotMatch(component, /1 \/ \{\{ productImages\.length \}\}/);
|
assert.doesNotMatch(component, /1 \/ \{\{ productImages\.length \}\}/);
|
||||||
assert.doesNotMatch(component, /productImages\.slice/);
|
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.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\.sectionCode/);
|
||||||
assert.match(component, /section\.title/);
|
assert.match(component, /section\.title/);
|
||||||
assert.match(component, /section\.content/);
|
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 () => {
|
test("booking flow matches the visitor prototype while preserving requested controls", async () => {
|
||||||
const component = await readTicketFile("components/BookingFlow.vue");
|
const component = await readTicketFile("components/BookingFlow.vue");
|
||||||
const page = await readTicketFile("booking.vue");
|
const page = await readTicketFile("booking.vue");
|
||||||
|
|||||||
Reference in New Issue
Block a user