Merge branch 'aigc' of https://git.nianxx.cn/zoujing/YGChatCS into home3.0
# Conflicts: # src/pages-aigc/agreement/agreement.vue # src/pages-booking/index.vue # src/pages.json # src/request/api/AigcApi.js # src/utils/index.js 合并了代码
This commit is contained in:
@@ -1,41 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import {
|
||||
DETAIL_ROUTE_BY_TYPE,
|
||||
getLongTextGuideDetailRoute,
|
||||
} from "../src/pages/ChatModule/LongTextGuideCardPreview/detailRoutes.mjs";
|
||||
|
||||
const dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const rootDir = path.resolve(dirname, "..");
|
||||
|
||||
const expectedRoutes = {
|
||||
longTextCard: "/pages/ChatModule/LongTextGuideCardPreview/guide",
|
||||
longTextCardScenicSpot: "/pages/ChatModule/LongTextGuideCardPreview/poi",
|
||||
longTextCardRoute: "/pages/ChatModule/LongTextGuideCardPreview/route",
|
||||
longTextCardSnap: "/pages/ChatModule/LongTextGuideCardPreview/photo",
|
||||
longTextCardFullDoc: "/pages/ChatModule/LongTextGuideCardPreview/fullDoc",
|
||||
};
|
||||
|
||||
assert.deepEqual(DETAIL_ROUTE_BY_TYPE, expectedRoutes);
|
||||
|
||||
for (const [type, route] of Object.entries(expectedRoutes)) {
|
||||
assert.equal(getLongTextGuideDetailRoute(type), route);
|
||||
|
||||
const pageFile = path.join(rootDir, "src", `${route.replace(/^\//, "")}.vue`);
|
||||
assert.ok(fs.existsSync(pageFile), `Missing page file for ${type}: ${pageFile}`);
|
||||
}
|
||||
|
||||
const pagesJson = JSON.parse(
|
||||
fs.readFileSync(path.join(rootDir, "src/pages.json"), "utf8"),
|
||||
);
|
||||
const registeredPages = new Set(pagesJson.pages.map((item) => item.path));
|
||||
|
||||
for (const route of Object.values(expectedRoutes)) {
|
||||
assert.ok(
|
||||
registeredPages.has(route.replace(/^\//, "")),
|
||||
`Route is not registered in src/pages.json: ${route}`,
|
||||
);
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||
const componentPath = resolve(
|
||||
scriptDir,
|
||||
"../src/pages/Discovery/components/CardSwiper/index.vue"
|
||||
);
|
||||
const source = readFileSync(componentPath, "utf8");
|
||||
|
||||
assert.match(
|
||||
source,
|
||||
/@touchmove\.stop\.prevent="handleTouchMove"/,
|
||||
"CardSwiper should keep native mini-program touchmove capture behavior"
|
||||
);
|
||||
|
||||
const touchStartMatch = source.match(
|
||||
/const\s+handleTouchStart\s*=\s*\(event\)\s*=>\s*{([\s\S]*?)\n};/
|
||||
);
|
||||
|
||||
assert.ok(touchStartMatch, "CardSwiper should define handleTouchStart");
|
||||
|
||||
assert.ok(
|
||||
!/if\s*\([^)]*isRecycling\.value[^)]*\)\s*return/.test(touchStartMatch[1]),
|
||||
"CardSwiper should accept the next touchstart during the recycle frame so quick follow-up swipes are not dropped"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
/clearFillFadeTimer\(\)/.test(touchStartMatch[1]) &&
|
||||
/clearRecycleTimer\(\)/.test(touchStartMatch[1]) &&
|
||||
/isRecycling\.value\s*=\s*false/.test(touchStartMatch[1]) &&
|
||||
/fillFadeKey\.value\s*=\s*["']["']/.test(touchStartMatch[1]),
|
||||
"CardSwiper should cancel recycle and decorative fill fade before starting a new gesture"
|
||||
);
|
||||
@@ -1,39 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||
const componentPath = resolve(
|
||||
scriptDir,
|
||||
"../src/pages/ChatMain/ChatInputArea/index.vue"
|
||||
);
|
||||
const source = readFileSync(componentPath, "utf8");
|
||||
|
||||
const handlerMatch = source.match(
|
||||
/const\s+handleVoiceTouchEndFromContainer\s*=\s*\(\)\s*=>\s*{([\s\S]*?)\n};/
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
handlerMatch,
|
||||
"ChatInputArea should define handleVoiceTouchEndFromContainer"
|
||||
);
|
||||
|
||||
const handlerBody = handlerMatch[1];
|
||||
|
||||
assert.ok(
|
||||
!/speechProvider\s*!==\s*["']app["']/.test(handlerBody),
|
||||
"container touchend must not ignore WeChat recordings because the pressed button can unmount while RecordingWaveBtn is shown"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
/hasActiveVoiceRecognition\(\)/.test(handlerBody),
|
||||
"container touchend should only stop when a voice recognition session is active"
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
/stopActiveVoiceRecognition\(\{\s*shouldSend:\s*true\s*\}\)/s.test(
|
||||
handlerBody
|
||||
),
|
||||
"container touchend should stop the active recognition and send recognized text"
|
||||
);
|
||||
@@ -1,207 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { dirname, resolve } from "node:path";
|
||||
import { pathToFileURL } from "node:url";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||
const componentPath = resolve(
|
||||
scriptDir,
|
||||
"../src/pages/ChatMain/ChatLongAnswer/ParsedValueView.vue"
|
||||
);
|
||||
const detailPagePath = resolve(
|
||||
scriptDir,
|
||||
"../src/pages/ChatMain/ChatLongAnswer/index.vue"
|
||||
);
|
||||
const longTextCardPath = resolve(scriptDir, "../src/utils/longTextCard.js");
|
||||
|
||||
const componentSource = readFileSync(componentPath, "utf8");
|
||||
const detailPageSource = readFileSync(detailPagePath, "utf8");
|
||||
const longTextCardSource = readFileSync(longTextCardPath, "utf8");
|
||||
const {
|
||||
appendLongTextChunk,
|
||||
createLongTextData,
|
||||
getLongTextSections,
|
||||
} = await import(pathToFileURL(longTextCardPath));
|
||||
|
||||
assert.doesNotMatch(
|
||||
longTextCardSource,
|
||||
/normalizeLongText/,
|
||||
"longTextCard should not contain field-specific normalize helpers"
|
||||
);
|
||||
|
||||
assert.doesNotMatch(
|
||||
longTextCardSource,
|
||||
/spot_longitude|spot_latitude|spot_tag/,
|
||||
"longTextCard should not know spot_locate child field names"
|
||||
);
|
||||
|
||||
assert.doesNotMatch(
|
||||
componentSource,
|
||||
/isGenericObjectSectionField/,
|
||||
"ParsedValueView should use renderFieldEntries instead of a separate generic object branch"
|
||||
);
|
||||
|
||||
assert.match(
|
||||
componentSource,
|
||||
/getRenderEntriesForObject/,
|
||||
"ParsedValueView should derive object rows through the existing entry renderer"
|
||||
);
|
||||
|
||||
assert.doesNotMatch(
|
||||
componentSource,
|
||||
/STRUCTURED_SECTION_CONFIG/,
|
||||
"ParsedValueView should not require per-field structured section config"
|
||||
);
|
||||
|
||||
assert.doesNotMatch(
|
||||
componentSource,
|
||||
/LONG_TEXT_KEYS\.(preparationSection|sectionSuggestion|pitfallSection|viewSection|suggestionSection|lightReminder|photoSpotSection|phoneSection|decisionSection|routeWarning|tourRoutes|facilitiesAlongTheWay)/,
|
||||
"ParsedValueView should not reference ordinary section keys individually"
|
||||
);
|
||||
|
||||
assert.doesNotMatch(
|
||||
componentSource,
|
||||
/content-body-section-dot|content-body-section-list/,
|
||||
"generic object arrays should keep the original list-card rendering style"
|
||||
);
|
||||
|
||||
assert.doesNotMatch(
|
||||
componentSource,
|
||||
/entry\.type\s*===\s*'(question-suggest|photo-list|aigc-componet|commodity-list|spot-locate)'/,
|
||||
"renderFieldEntries should only render generic text/image/list entries"
|
||||
);
|
||||
|
||||
assert.doesNotMatch(
|
||||
componentSource,
|
||||
/createSpecialFieldEntry/,
|
||||
"dedicated long text components should be selected by top-level computed branches only"
|
||||
);
|
||||
|
||||
assert.match(
|
||||
componentSource,
|
||||
/shouldRenderQuestionSuggest/,
|
||||
"top-level question_suggest fields should render through the dedicated chips"
|
||||
);
|
||||
|
||||
assert.match(
|
||||
componentSource,
|
||||
/shouldRenderCommodityList/,
|
||||
"top-level commodity_list fields should render through the dedicated product list"
|
||||
);
|
||||
|
||||
assert.match(
|
||||
componentSource,
|
||||
/shouldRenderPhotoList/,
|
||||
"top-level photo_list fields should render through the dedicated photo swiper"
|
||||
);
|
||||
|
||||
assert.match(
|
||||
componentSource,
|
||||
/shouldRenderAigcComponet/,
|
||||
"top-level aigc_componet fields should render through the dedicated AIGC card"
|
||||
);
|
||||
|
||||
assert.match(
|
||||
componentSource,
|
||||
/shouldRenderSpotLocate/,
|
||||
"top-level spot_locate fields should render through the dedicated POI card"
|
||||
);
|
||||
|
||||
assert.doesNotMatch(
|
||||
componentSource,
|
||||
/createRenderEntry\(key,\s*entryKey,\s*value\[key\]\)/,
|
||||
"nested object keys should not trigger dedicated long text components"
|
||||
);
|
||||
|
||||
assert.match(
|
||||
componentSource,
|
||||
/openMap\(spotLocateValue\)/,
|
||||
"ParsedValueView should render spot_locate with the dedicated POI action card"
|
||||
);
|
||||
|
||||
assert.match(
|
||||
componentSource,
|
||||
/getSpotLocateValue/,
|
||||
"ParsedValueView should own spot_locate value shaping"
|
||||
);
|
||||
|
||||
assert.doesNotMatch(
|
||||
componentSource,
|
||||
/spot_longitude:|spot_latitude:|spot_tag:/,
|
||||
"ParsedValueView should not read malformed spot_locate keys"
|
||||
);
|
||||
|
||||
assert.match(
|
||||
componentSource,
|
||||
/shouldRenderQuestionSuggest/,
|
||||
"ParsedValueView should render question_suggest with dedicated FAQ chips"
|
||||
);
|
||||
|
||||
assert.match(
|
||||
componentSource,
|
||||
/getQuestionSuggestItems/,
|
||||
"ParsedValueView should own question_suggest value shaping"
|
||||
);
|
||||
|
||||
assert.match(
|
||||
componentSource,
|
||||
/sendReply\(question\)/,
|
||||
"question_suggest chips should send the selected follow-up question"
|
||||
);
|
||||
|
||||
assert.doesNotMatch(
|
||||
detailPageSource,
|
||||
/LONG_TEXT_KEYS\.content\b/,
|
||||
"long answer detail page should not reference removed content key"
|
||||
);
|
||||
|
||||
assert.match(
|
||||
componentSource,
|
||||
/LONG_TEXT_KEYS\.contentImage\b/,
|
||||
"ParsedValueView should render content_image with the dedicated image style"
|
||||
);
|
||||
|
||||
const ignoredFieldKeysMatch = componentSource.match(
|
||||
/const\s+IGNORED_FIELD_KEYS\s*=\s*\[([\s\S]*?)\];/
|
||||
);
|
||||
|
||||
assert.ok(
|
||||
ignoredFieldKeysMatch,
|
||||
"ParsedValueView should define IGNORED_FIELD_KEYS"
|
||||
);
|
||||
|
||||
assert.doesNotMatch(
|
||||
ignoredFieldKeysMatch[1],
|
||||
/["']content_summary["']/,
|
||||
"ParsedValueView should not ignore content_summary"
|
||||
);
|
||||
|
||||
const receivedKeys = [
|
||||
"tag",
|
||||
"title",
|
||||
"content_summary",
|
||||
"scene_image",
|
||||
"content_image",
|
||||
"view_section",
|
||||
"suggestion_section",
|
||||
"light_reminder",
|
||||
"spot_locate",
|
||||
"question_suggest",
|
||||
];
|
||||
|
||||
const longTextData = createLongTextData();
|
||||
|
||||
receivedKeys.forEach((key) => {
|
||||
appendLongTextChunk(longTextData, { contentKey: key, contentValue: key });
|
||||
});
|
||||
appendLongTextChunk(longTextData, {
|
||||
contentKey: "view_section_items",
|
||||
contentValue: "old flat field",
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
getLongTextSections(longTextData).map((section) => section.contentKey),
|
||||
[...receivedKeys, "view_section_items"],
|
||||
"long text detail sections should keep configured fields first and preserve unknown contentKey fields"
|
||||
);
|
||||
@@ -1,129 +0,0 @@
|
||||
import assert from "assert";
|
||||
import { readFileSync } from "fs";
|
||||
import { dirname, resolve } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
||||
const detailPagePath = resolve(
|
||||
scriptDir,
|
||||
"../src/pages/ChatMain/ChatLongAnswer/index.vue"
|
||||
);
|
||||
const parsedValuePath = resolve(
|
||||
scriptDir,
|
||||
"../src/pages/ChatMain/ChatLongAnswer/ParsedValueView.vue"
|
||||
);
|
||||
const detailStylePath = resolve(
|
||||
scriptDir,
|
||||
"../src/pages/ChatMain/ChatLongAnswer/styles/index.scss"
|
||||
);
|
||||
const skeletonComponentPath = resolve(
|
||||
scriptDir,
|
||||
"../src/pages/ChatMain/ChatLongAnswer/LongAnswerSectionSkeleton.vue"
|
||||
);
|
||||
|
||||
const detailPageSource = readFileSync(detailPagePath, "utf8");
|
||||
const parsedValueSource = readFileSync(parsedValuePath, "utf8");
|
||||
const detailStyleSource = readFileSync(detailStylePath, "utf8");
|
||||
const skeletonComponentSource = readFileSync(skeletonComponentPath, "utf8");
|
||||
|
||||
assert.match(
|
||||
detailPageSource,
|
||||
/const\s+streamFinished\s*=\s*ref/,
|
||||
"long answer page should track whether the forwarded stream has finished"
|
||||
);
|
||||
|
||||
assert.match(
|
||||
detailPageSource,
|
||||
/import\s+LongAnswerSectionSkeleton\s+from\s+"\.\/LongAnswerSectionSkeleton\.vue"/,
|
||||
"long answer page should use a sibling skeleton component"
|
||||
);
|
||||
|
||||
assert.doesNotMatch(
|
||||
detailPageSource,
|
||||
/DEFERRED_SPECIAL_SECTION_KEYS/,
|
||||
"long answer page should not use a special-key whitelist for section skeletons"
|
||||
);
|
||||
|
||||
assert.doesNotMatch(
|
||||
detailPageSource,
|
||||
/LONG_TEXT_KEYS\.(sceneImage|contentImage|spotLocate|questionSuggest|commodityList|photoList|aigcComponet)/,
|
||||
"section skeleton readiness should not be tied to named special keys"
|
||||
);
|
||||
|
||||
assert.match(
|
||||
detailPageSource,
|
||||
/<LongAnswerSectionSkeleton\s+v-if="shouldRenderSectionSkeleton\(section\)"/,
|
||||
"each section should decide whether to show its own skeleton inside the contentSections loop"
|
||||
);
|
||||
|
||||
assert.match(
|
||||
detailPageSource,
|
||||
/v-else-if="shouldUseParsedValueView\(section\)"/,
|
||||
"ParsedValueView should render after the per-section skeleton check"
|
||||
);
|
||||
|
||||
assert.match(
|
||||
detailPageSource,
|
||||
/getSectionIndex\(section\)/,
|
||||
"section readiness should use the section position to know when the next key has started"
|
||||
);
|
||||
|
||||
assert.match(
|
||||
detailPageSource,
|
||||
/hasSectionAfter\(section\)/,
|
||||
"a deferred section should become renderable when a later key arrives"
|
||||
);
|
||||
|
||||
assert.match(
|
||||
detailPageSource,
|
||||
/isSectionReady\(section\)/,
|
||||
"long answer page should centralize when a structured section is ready to render"
|
||||
);
|
||||
|
||||
assert.match(
|
||||
detailPageSource,
|
||||
/isNonStringSectionValue/,
|
||||
"structured skeletons should be driven by non-string parsed values"
|
||||
);
|
||||
|
||||
assert.match(
|
||||
detailPageSource,
|
||||
/isPendingStructuredText/,
|
||||
"pending JSON-like section strings should use the skeleton until parsed"
|
||||
);
|
||||
|
||||
assert.match(
|
||||
detailPageSource,
|
||||
/section\.parsedValue\s*!==\s*null\s*&&\s*isNonStringSectionValue\(section\.parsedValue\)/,
|
||||
"parsed non-string values should identify structured sections"
|
||||
);
|
||||
|
||||
assert.doesNotMatch(
|
||||
detailPageSource,
|
||||
/shouldRenderStreamingReceiver|class="long-answer-receiver"/,
|
||||
"long answer page should not use one global receiver box"
|
||||
);
|
||||
|
||||
assert.doesNotMatch(
|
||||
detailPageSource,
|
||||
/:defer-special-render=/,
|
||||
"ParsedValueView should not hide deferred sections internally"
|
||||
);
|
||||
|
||||
assert.doesNotMatch(
|
||||
parsedValueSource,
|
||||
/deferSpecialRender|shouldDeferSpecialRender/,
|
||||
"ParsedValueView should stay focused on rendering values, not streaming readiness"
|
||||
);
|
||||
|
||||
assert.match(
|
||||
skeletonComponentSource,
|
||||
/\.long-answer-section-skeleton\s*{/,
|
||||
"section skeleton styles should live in the sibling skeleton component"
|
||||
);
|
||||
|
||||
assert.doesNotMatch(
|
||||
detailStyleSource,
|
||||
/\.long-answer-section-skeleton\b|\.long-answer-receiver\b/,
|
||||
"long answer page styles should not contain skeleton implementation details"
|
||||
);
|
||||
@@ -1,200 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const source = await readFile(
|
||||
resolve("src/pages/ChatMain/ChatLongAnswer/ParsedValueView.vue"),
|
||||
"utf8"
|
||||
);
|
||||
const styleSource = await readFile(
|
||||
resolve("src/pages/ChatMain/ChatLongAnswer/styles/ParsedValueView.scss"),
|
||||
"utf8"
|
||||
);
|
||||
|
||||
const forbiddenCompatibilitySnippets = [
|
||||
"valueObj.image_url",
|
||||
"valueObj.image,",
|
||||
"valueObj.image_id",
|
||||
"valueObj.content_image",
|
||||
"valueObj.sopt_name",
|
||||
"valueObj.name",
|
||||
"valueObj.title",
|
||||
"valueObj.sopt_description",
|
||||
"valueObj.description",
|
||||
"valueObj.desc",
|
||||
"valueObj.sopt_longitude",
|
||||
"valueObj.longitude",
|
||||
"valueObj.lng",
|
||||
"valueObj.sopt_latitude",
|
||||
"valueObj.latitude",
|
||||
"valueObj.lat",
|
||||
"valueObj.sopt_tag",
|
||||
"valueObj.tag",
|
||||
"valueObj.type",
|
||||
"value?.questions",
|
||||
"value?.items",
|
||||
"value?.list",
|
||||
];
|
||||
|
||||
for (const snippet of forbiddenCompatibilitySnippets) {
|
||||
assert.equal(
|
||||
source.includes(snippet),
|
||||
false,
|
||||
`ParsedValueView should not guess compatibility field: ${snippet}`
|
||||
);
|
||||
}
|
||||
|
||||
const requiredStrictRenderingSnippets = [
|
||||
"LONG_TEXT_KEYS.sceneImage",
|
||||
"LONG_TEXT_KEYS.contentImage",
|
||||
"LONG_TEXT_KEYS.commodityList",
|
||||
"LONG_TEXT_KEYS.photoList",
|
||||
"LONG_TEXT_KEYS.aigcComponet",
|
||||
"shouldRenderSpotLocate",
|
||||
"shouldRenderQuestionSuggest",
|
||||
"shouldRenderCommodityList",
|
||||
"shouldRenderPhotoList",
|
||||
"shouldRenderAigcComponet",
|
||||
"getRenderEntriesForObject",
|
||||
"commodity.commodity_id",
|
||||
"commodity.commodity_name",
|
||||
"commodity.commodity_price",
|
||||
"commodity.commodity_tag",
|
||||
"commodity.commodity_photo",
|
||||
"<swiper",
|
||||
"<swiper-item",
|
||||
"photo.photo_name",
|
||||
"photo.photo_description",
|
||||
"photo.photo_url",
|
||||
"aigc.background",
|
||||
"aigc.title",
|
||||
"aigc.description",
|
||||
"aigc.jumpUrl",
|
||||
"jumpAigcClick",
|
||||
"getAccessToken",
|
||||
"navigateTo",
|
||||
"content-body-list-card",
|
||||
];
|
||||
|
||||
for (const snippet of requiredStrictRenderingSnippets) {
|
||||
assert.equal(
|
||||
source.includes(snippet),
|
||||
true,
|
||||
`ParsedValueView should render exact strict field: ${snippet}`
|
||||
);
|
||||
}
|
||||
|
||||
const requiredSingleCommodityOverviewSnippets = [
|
||||
':scroll-x="!isSingleCommodity"',
|
||||
"'overview-combo-card': isSingleCommodity",
|
||||
'v-if="isSingleCommodity"',
|
||||
'class="overview-combo-body"',
|
||||
'class="overview-combo-cover"',
|
||||
'class="overview-combo-info"',
|
||||
'class="overview-combo-name"',
|
||||
'class="overview-combo-desc"',
|
||||
'class="overview-combo-price"',
|
||||
'class="overview-combo-currency"',
|
||||
'class="overview-combo-button"',
|
||||
];
|
||||
|
||||
for (const snippet of requiredSingleCommodityOverviewSnippets) {
|
||||
assert.equal(
|
||||
source.includes(snippet),
|
||||
true,
|
||||
`ParsedValueView single commodity should use overview combo layout: ${snippet}`
|
||||
);
|
||||
}
|
||||
|
||||
const requiredSingleCommodityOverviewStyleSnippets = [
|
||||
".overview-combo-card",
|
||||
".overview-combo-body",
|
||||
"display: flex;",
|
||||
".overview-combo-cover",
|
||||
"width: 78px;",
|
||||
"height: 78px;",
|
||||
".overview-combo-info",
|
||||
".overview-combo-name",
|
||||
".overview-combo-desc",
|
||||
".overview-combo-price",
|
||||
".overview-combo-currency",
|
||||
".overview-combo-button",
|
||||
"height: 40px;",
|
||||
];
|
||||
|
||||
for (const snippet of requiredSingleCommodityOverviewStyleSnippets) {
|
||||
assert.equal(
|
||||
styleSource.includes(snippet),
|
||||
true,
|
||||
`ParsedValueView single commodity should define overview combo style: ${snippet}`
|
||||
);
|
||||
}
|
||||
|
||||
const removedInnerSpecialEntrySnippets = [
|
||||
"createSpecialFieldEntry",
|
||||
"entry.type === 'question-suggest'",
|
||||
"entry.type === 'photo-list'",
|
||||
"entry.type === 'aigc-componet'",
|
||||
"entry.type === 'commodity-list'",
|
||||
"entry.type === 'spot-locate'",
|
||||
];
|
||||
|
||||
for (const snippet of removedInnerSpecialEntrySnippets) {
|
||||
assert.equal(
|
||||
source.includes(snippet),
|
||||
false,
|
||||
`ParsedValueView should keep special fields at the top level only: ${snippet}`
|
||||
);
|
||||
}
|
||||
|
||||
const removedFlatFieldSnippets = [
|
||||
"preparationSectionTitle",
|
||||
"preparationSectionItems",
|
||||
"sectionSuggestionTitle",
|
||||
"sectionSuggestionContent",
|
||||
"pitfallSectionTitle",
|
||||
"pitfallSectionItems",
|
||||
"viewSectionTitle",
|
||||
"viewSectionItems",
|
||||
"suggestionSectionTitle",
|
||||
"suggestionSectionContent",
|
||||
"lightReminderTitle",
|
||||
"lightReminderItems",
|
||||
];
|
||||
|
||||
for (const snippet of removedFlatFieldSnippets) {
|
||||
assert.equal(
|
||||
source.includes(`LONG_TEXT_KEYS.${snippet}`),
|
||||
false,
|
||||
`ParsedValueView should not reference removed flat field: ${snippet}`
|
||||
);
|
||||
}
|
||||
|
||||
const removedStructuredConfigSnippets = [
|
||||
"STRUCTURED_SECTION_CONFIG",
|
||||
"isGenericObjectSectionField",
|
||||
"content-body-section-dot",
|
||||
"content-body-section-list",
|
||||
"LONG_TEXT_KEYS.preparationSection",
|
||||
"LONG_TEXT_KEYS.sectionSuggestion",
|
||||
"LONG_TEXT_KEYS.pitfallSection",
|
||||
"LONG_TEXT_KEYS.viewSection",
|
||||
"LONG_TEXT_KEYS.suggestionSection",
|
||||
"LONG_TEXT_KEYS.lightReminder",
|
||||
"LONG_TEXT_KEYS.photoSpotSection",
|
||||
"LONG_TEXT_KEYS.phoneSection",
|
||||
"LONG_TEXT_KEYS.decisionSection",
|
||||
"LONG_TEXT_KEYS.routeWarning",
|
||||
"LONG_TEXT_KEYS.tourRoutes",
|
||||
"LONG_TEXT_KEYS.facilitiesAlongTheWay",
|
||||
];
|
||||
|
||||
for (const snippet of removedStructuredConfigSnippets) {
|
||||
assert.equal(
|
||||
source.includes(snippet),
|
||||
false,
|
||||
`ParsedValueView should use generic section rendering instead of configured field: ${snippet}`
|
||||
);
|
||||
}
|
||||
|
||||
console.log("ParsedValueView strict field checks passed");
|
||||
@@ -1,114 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
const helperUrl = new URL("../src/utils/discoveryPanelScroll.js", import.meta.url);
|
||||
const chatMainListUrl = new URL("../src/pages/ChatMain/ChatMainList/index.vue", import.meta.url);
|
||||
const helperSource = await readFile(helperUrl, "utf8");
|
||||
const chatMainListSource = await readFile(chatMainListUrl, "utf8");
|
||||
const helperModule = await import(
|
||||
`data:text/javascript;charset=utf-8,${encodeURIComponent(helperSource)}`
|
||||
);
|
||||
|
||||
const {
|
||||
canExpandDiscoveryPanelFromScroll,
|
||||
getDiscoveryPanelCollapseState,
|
||||
} = helperModule;
|
||||
|
||||
assert.equal(
|
||||
getDiscoveryPanelCollapseState({
|
||||
currentScrollTop: 24,
|
||||
lastScrollTop: 0,
|
||||
isCollapsed: false,
|
||||
threshold: 16,
|
||||
}),
|
||||
true,
|
||||
"scrolling up into discovery content should collapse the welcome panel"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
getDiscoveryPanelCollapseState({
|
||||
currentScrollTop: 18,
|
||||
lastScrollTop: 15,
|
||||
isCollapsed: false,
|
||||
threshold: 16,
|
||||
}),
|
||||
true,
|
||||
"slow upward scrolling past the threshold should collapse the welcome panel"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
getDiscoveryPanelCollapseState({
|
||||
currentScrollTop: 40,
|
||||
lastScrollTop: 64,
|
||||
isCollapsed: true,
|
||||
threshold: 16,
|
||||
allowExpand: true,
|
||||
}),
|
||||
false,
|
||||
"scrolling down should reveal the welcome panel"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
getDiscoveryPanelCollapseState({
|
||||
currentScrollTop: 40,
|
||||
lastScrollTop: 64,
|
||||
isCollapsed: true,
|
||||
threshold: 16,
|
||||
allowExpand: false,
|
||||
}),
|
||||
true,
|
||||
"layout-driven reverse scroll after release should not reveal the welcome panel"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
getDiscoveryPanelCollapseState({
|
||||
currentScrollTop: 63,
|
||||
lastScrollTop: 64,
|
||||
isCollapsed: true,
|
||||
threshold: 16,
|
||||
}),
|
||||
true,
|
||||
"small scroll jitter should keep the current panel state"
|
||||
);
|
||||
|
||||
assert.match(
|
||||
chatMainListSource,
|
||||
/@touchmove\.capture="handleDiscoveryPanelTouchMove"/,
|
||||
"discovery shell should capture touchmove before nested scroll-view consumes the gesture"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
canExpandDiscoveryPanelFromScroll({
|
||||
isTouching: false,
|
||||
touchDirection: "",
|
||||
lastDownGestureAt: 1000,
|
||||
now: 1400,
|
||||
gestureRetainMs: 800,
|
||||
}),
|
||||
true,
|
||||
"momentum scroll after a down gesture should still be allowed to reveal the welcome panel"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
canExpandDiscoveryPanelFromScroll({
|
||||
isTouching: false,
|
||||
touchDirection: "",
|
||||
lastDownGestureAt: 1000,
|
||||
now: 1901,
|
||||
gestureRetainMs: 800,
|
||||
}),
|
||||
false,
|
||||
"old reverse scroll should not be treated as an active down gesture"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
canExpandDiscoveryPanelFromScroll({
|
||||
isTouching: true,
|
||||
touchDirection: "up",
|
||||
lastDownGestureAt: 1000,
|
||||
now: 1200,
|
||||
gestureRetainMs: 800,
|
||||
}),
|
||||
false,
|
||||
"an active upward gesture should block reveal even if a down gesture happened earlier"
|
||||
);
|
||||
@@ -1,273 +0,0 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { resolve } from "node:path";
|
||||
|
||||
const source = await readFile(resolve("src/utils/longTextCard.js"), "utf8");
|
||||
const moduleUrl = `data:text/javascript;base64,${Buffer.from(source).toString("base64")}`;
|
||||
const longTextCard = await import(moduleUrl);
|
||||
|
||||
assert.doesNotMatch(
|
||||
source,
|
||||
/normalizeLongText/,
|
||||
"longTextCard should not contain field-specific normalize helpers"
|
||||
);
|
||||
|
||||
assert.doesNotMatch(
|
||||
source,
|
||||
/spot_longitude|spot_latitude|spot_tag/,
|
||||
"longTextCard should not know spot_locate child field names"
|
||||
);
|
||||
|
||||
const {
|
||||
LONG_TEXT_FIELD_CONFIG,
|
||||
LONG_TEXT_KEYS,
|
||||
appendLongTextChunk,
|
||||
createLongTextData,
|
||||
getLongTextParsedValue,
|
||||
getLongTextSections,
|
||||
getLongTextValue,
|
||||
hasLongTextExtraSections,
|
||||
parseLongTextDisplayValue,
|
||||
sanitizeLongTextDisplayValue,
|
||||
hasLongTextDisplayValue,
|
||||
formatLongTextDisplayValue,
|
||||
} = longTextCard;
|
||||
|
||||
assert.equal(
|
||||
source.includes("REMOVED_LONG_TEXT_FIELD_KEYS"),
|
||||
false,
|
||||
"longTextCard should not carry a removed field blacklist"
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
source.includes("hasLongTextChunkPayload"),
|
||||
false,
|
||||
"ChatMainList should not ask longTextCard to inspect plain data.content payloads"
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
parseLongTextDisplayValue('[ "see bridge", "hear water" ]'),
|
||||
["see bridge", "hear water"],
|
||||
"serialized arrays should be parsed for display"
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
parseLongTextDisplayValue('{"spot_name":"bridge","spot_longitude":107.712345}'),
|
||||
{ spot_name: "bridge", spot_longitude: 107.712345 },
|
||||
"serialized objects should be parsed for display"
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
sanitizeLongTextDisplayValue(
|
||||
{
|
||||
preparation_section: {
|
||||
preparation_section_title: "keep",
|
||||
preparation_section_items: '[ "check piers", "count arches" ]',
|
||||
},
|
||||
nested: {
|
||||
title: "keep",
|
||||
},
|
||||
},
|
||||
[]
|
||||
),
|
||||
{
|
||||
preparation_section: {
|
||||
preparation_section_title: "keep",
|
||||
preparation_section_items: ["check piers", "count arches"],
|
||||
},
|
||||
nested: {
|
||||
title: "keep",
|
||||
},
|
||||
},
|
||||
"sanitizing should parse JSON-like strings and ignore configured keys"
|
||||
);
|
||||
|
||||
assert.equal(hasLongTextDisplayValue('["follow up"]'), true);
|
||||
assert.equal(hasLongTextDisplayValue({ a: "", b: [" ", null] }), false);
|
||||
assert.equal(formatLongTextDisplayValue(true), "\u662f");
|
||||
assert.equal(formatLongTextDisplayValue({ title: "bridge" }), '{"title":"bridge"}');
|
||||
|
||||
const expectedNewKeys = {
|
||||
tag: "tag",
|
||||
title: "title",
|
||||
contentSummary: "content_summary",
|
||||
sceneImage: "scene_image",
|
||||
preparationSection: "preparation_section",
|
||||
sectionSuggestion: "section_suggestion",
|
||||
pitfallSection: "pitfall_section",
|
||||
commodityList: "commodity_list",
|
||||
contentImage: "content_image",
|
||||
viewSection: "view_section",
|
||||
suggestionSection: "suggestion_section",
|
||||
lightReminder: "light_reminder",
|
||||
spotLocate: "spot_locate",
|
||||
photoSpotSection: "photo_spot_section",
|
||||
phoneSection: "phone_section",
|
||||
photoList: "photo_list",
|
||||
aigcComponet: "aigc_componet",
|
||||
decisionSection: "decision_section",
|
||||
routeWarning: "route_warning",
|
||||
tourRoutes: "tour_routes",
|
||||
facilitiesAlongTheWay: "facilities_along_the_way",
|
||||
questionSuggest: "question_suggest",
|
||||
};
|
||||
|
||||
for (const [keyName, keyValue] of Object.entries(expectedNewKeys)) {
|
||||
assert.equal(LONG_TEXT_KEYS[keyName], keyValue, `${keyName} should be registered`);
|
||||
assert.equal(
|
||||
LONG_TEXT_FIELD_CONFIG.some((item) => item.key === keyValue),
|
||||
true,
|
||||
`${keyValue} should be in LONG_TEXT_FIELD_CONFIG`
|
||||
);
|
||||
}
|
||||
|
||||
const removedKeys = [
|
||||
"content",
|
||||
"guideConclusion",
|
||||
"keyFacts",
|
||||
"preparationSectionTitle",
|
||||
"preparationSectionItems",
|
||||
"sectionSuggestionTitle",
|
||||
"sectionSuggestionContent",
|
||||
"pitfallSectionTitle",
|
||||
"pitfallSectionItems",
|
||||
"viewSectionTitle",
|
||||
"viewSectionItems",
|
||||
"suggestionSectionTitle",
|
||||
"suggestionSectionContent",
|
||||
"lightReminderTitle",
|
||||
"lightReminderItems",
|
||||
"components",
|
||||
"actionZone",
|
||||
];
|
||||
|
||||
for (const keyName of removedKeys) {
|
||||
assert.equal(
|
||||
Object.prototype.hasOwnProperty.call(LONG_TEXT_KEYS, keyName),
|
||||
false,
|
||||
`${keyName} should be removed from LONG_TEXT_KEYS`
|
||||
);
|
||||
}
|
||||
|
||||
const oldFlatFieldValues = [
|
||||
"preparation_section_title",
|
||||
"preparation_section_items",
|
||||
"view_section_title",
|
||||
"view_section_items",
|
||||
];
|
||||
|
||||
for (const keyValue of oldFlatFieldValues) {
|
||||
assert.equal(
|
||||
LONG_TEXT_FIELD_CONFIG.some((item) => item.key === keyValue),
|
||||
false,
|
||||
`${keyValue} should be removed from LONG_TEXT_FIELD_CONFIG`
|
||||
);
|
||||
}
|
||||
|
||||
const longTextData = createLongTextData();
|
||||
appendLongTextChunk(longTextData, { contentKey: "title", contentValue: "漂流攻略" });
|
||||
appendLongTextChunk(longTextData, {
|
||||
contentKey: "preparation_section",
|
||||
contentValue: {
|
||||
preparation_section_title: "下水前",
|
||||
preparation_section_items: ["带手机防水袋", "穿涉水鞋"],
|
||||
},
|
||||
});
|
||||
appendLongTextChunk(longTextData, {
|
||||
contentKey: "future_section",
|
||||
contentValue: {
|
||||
future_section_title: "未来新增字段",
|
||||
future_section_items: ["不改 LONG_TEXT_KEYS 也展示"],
|
||||
},
|
||||
});
|
||||
appendLongTextChunk(longTextData, {
|
||||
contentKey: "content_image",
|
||||
contentValue: "https://oss.nianxx.cn/XiaoQiKong/GQ00001.jpg",
|
||||
});
|
||||
appendLongTextChunk(longTextData, {
|
||||
contentKey: "spot_locate",
|
||||
contentValue: '{"spot_name":"卧龙潭","spot_longitude":"107.712345","spot_latitude":"25.251234","spot_tag":"景点"}',
|
||||
});
|
||||
|
||||
assert.equal(getLongTextValue(longTextData, "title"), "漂流攻略");
|
||||
assert.deepEqual(
|
||||
getLongTextParsedValue(longTextData, "preparation_section"),
|
||||
{
|
||||
preparation_section_title: "下水前",
|
||||
preparation_section_items: ["带手机防水袋", "穿涉水鞋"],
|
||||
},
|
||||
"object contentValue should be serialized and parsed"
|
||||
);
|
||||
assert.deepEqual(
|
||||
getLongTextSections(longTextData).map((section) => section.contentKey),
|
||||
["title", "preparation_section", "future_section", "content_image", "spot_locate"],
|
||||
"sections should place unknown fields by receive order between configured fields"
|
||||
);
|
||||
assert.equal(hasLongTextExtraSections(longTextData), true);
|
||||
|
||||
assert.deepEqual(
|
||||
getLongTextParsedValue(longTextData, "future_section"),
|
||||
{
|
||||
future_section_title: "未来新增字段",
|
||||
future_section_items: ["不改 LONG_TEXT_KEYS 也展示"],
|
||||
},
|
||||
"future top-level fields should keep parsed values"
|
||||
);
|
||||
|
||||
const mergedPayload = {
|
||||
tag: "攻略",
|
||||
title: "合并结构标题",
|
||||
content_summary: "合并结构摘要",
|
||||
preparation_section: {
|
||||
preparation_section_title: "下水前",
|
||||
preparation_section_items: ["带手机防水袋", "穿涉水鞋"],
|
||||
},
|
||||
spot_locate: {
|
||||
spot_name: "卧龙潭",
|
||||
spot_longitude: "107.712345",
|
||||
spot_latitude: "25.251234",
|
||||
spot_tag: "景点",
|
||||
},
|
||||
photo_list: [
|
||||
{
|
||||
photo_name: "桥边机位",
|
||||
photo_description: "站在桥头拍",
|
||||
photo_url: "https://example.com/photo.png",
|
||||
},
|
||||
],
|
||||
photo_spot_section: "{\n\"photo_spot_section_title\":\"四个不会错的机位\",\n\"photo_spot_items\":\"- 涵碧潭半清半浊\\n- 断桥飞瀑错位玩\",\n\"best_time_suggestion\":\"下午 2 点后人少\"\n}",
|
||||
phone_section: "{\n\"phone_section_title\":\"手机党看这里\",\n\"phone_section_items\":\"- 手机倒过来贴近水面\\n- 瀑布用长曝光\"\n}",
|
||||
aigc_componet: {
|
||||
background: "https://example.com/aigc.png",
|
||||
title: "AIGC合影",
|
||||
description: "生成合影",
|
||||
jumpUrl: "https://example.com/aigc",
|
||||
},
|
||||
question_suggest: ["还要准备什么?"],
|
||||
future_section: {
|
||||
future_section_title: "后续新增模块",
|
||||
future_section_items: ["仍然展示"],
|
||||
},
|
||||
};
|
||||
|
||||
const mergedLongTextData = createLongTextData();
|
||||
appendLongTextChunk(mergedLongTextData, {
|
||||
contentKey: "photo_card_payload",
|
||||
contentValue: mergedPayload,
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
getLongTextSections(mergedLongTextData).map((section) => section.contentKey),
|
||||
[
|
||||
"photo_card_payload",
|
||||
],
|
||||
"unknown contentKey fields should be kept as a single generic section"
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
getLongTextParsedValue(mergedLongTextData, "photo_card_payload"),
|
||||
mergedPayload,
|
||||
"unknown contentKey object values should keep parsed values"
|
||||
);
|
||||
|
||||
console.log("longTextCard display helpers passed");
|
||||
Reference in New Issue
Block a user