补齐平台订阅、外部登录、CRM 同步与用户端页面;Nginx 转发用户端接口,登录页使用 Google 与 Apple 标志。 Co-authored-by: Cursor <cursoragent@cursor.com>
811 行
27 KiB
TypeScript
811 行
27 KiB
TypeScript
/**
|
||
* OmniCard AI (CRMmp) - mpuniapp 文章详情第一版测试套件 (TDD Red -> Green)
|
||
*
|
||
* 严格按照 AGENTS.md 3.3、4.3、5.1 规范与 PRD pages/portal/article-detail 设计编写
|
||
*/
|
||
|
||
type BentoBlockFixture = {
|
||
id: number | string;
|
||
cardId: number | string;
|
||
type: string;
|
||
colSpan: number;
|
||
rowSpan: number;
|
||
sortOrder: number;
|
||
title?: string;
|
||
content?: string;
|
||
mediaUrl?: string;
|
||
linkUrl?: string;
|
||
};
|
||
|
||
type ArticleDetailState = {
|
||
cardId: string;
|
||
blockId: string;
|
||
loading: boolean;
|
||
article: BentoBlockFixture | null;
|
||
title: string;
|
||
content: string;
|
||
mediaUrl: string;
|
||
linkUrl: string;
|
||
isHtmlContent: boolean;
|
||
empty: boolean;
|
||
errorMessage: string;
|
||
openGraph: {
|
||
title: string;
|
||
description: string;
|
||
image: string;
|
||
};
|
||
authorName?: string;
|
||
authorTitle?: string;
|
||
authorCompany?: string;
|
||
authorAvatar?: string;
|
||
readTimeMinutes?: number;
|
||
summaryText?: string;
|
||
scrollDepth?: number;
|
||
enterTime?: number;
|
||
visitorId?: string;
|
||
};
|
||
|
||
type ArticleShareFields = {
|
||
ctaLabel: string;
|
||
copiedToast: string;
|
||
shareUrl: string;
|
||
title: string;
|
||
description: string;
|
||
image: string;
|
||
};
|
||
|
||
type ArticleDetailModule = {
|
||
createArticleDetailState: () => ArticleDetailState;
|
||
applyArticleDetailOptions: (
|
||
state: ArticleDetailState,
|
||
options: { cardId?: string; blockId?: string; id?: string } | null
|
||
) => boolean;
|
||
pickArticleBlock: (
|
||
blocks: BentoBlockFixture[] | null | undefined,
|
||
blockId: string
|
||
) => BentoBlockFixture | null;
|
||
looksLikeHtml: (content: string) => boolean;
|
||
buildArticleOpenGraph: (article: BentoBlockFixture | null) => {
|
||
title: string;
|
||
description: string;
|
||
image: string;
|
||
};
|
||
resolveShareUrl: (linkUrl: string, pageUrl: string) => string;
|
||
getArticleShareFields: (state: ArticleDetailState, pageUrl: string) => ArticleShareFields;
|
||
loadArticleDetail: (state: ArticleDetailState) => Promise<BentoBlockFixture | null>;
|
||
copyArticleShareLink: (state: ArticleDetailState, pageUrl?: string) => boolean;
|
||
shareArticleViaSystem: (
|
||
state: ArticleDetailState,
|
||
pageUrl?: string
|
||
) => Promise<boolean>;
|
||
resolveArticleDetailText: (key: string, locale?: string) => string;
|
||
updateArticleScrollDepth: (
|
||
state: ArticleDetailState,
|
||
scrollTop: number,
|
||
scrollHeight: number,
|
||
windowHeight: number
|
||
) => number;
|
||
reportArticleStay: (state: ArticleDetailState, scrollDepth?: number) => Promise<unknown>;
|
||
navToAuthorPortal: (state: ArticleDetailState) => void;
|
||
};
|
||
|
||
const SHARE_SUCCESS_TOAST_RE = /已分享|Shared(?! content)|Partagé|Compartido|Geteilt|共有しました/i;
|
||
|
||
function assertNoShareSuccessToast(toasts: UniToastOption[]): void {
|
||
expect(toasts.some(t => SHARE_SUCCESS_TOAST_RE.test(t.title))).toBe(false);
|
||
}
|
||
|
||
type UniToastOption = {
|
||
title: string;
|
||
icon?: string;
|
||
};
|
||
|
||
type UniRequestSuccessRes = {
|
||
statusCode: number;
|
||
data: unknown;
|
||
header: Record<string, string>;
|
||
};
|
||
|
||
type UniRequestOption = {
|
||
url: string;
|
||
method?: string;
|
||
data?: unknown;
|
||
success?: (res: UniRequestSuccessRes) => void;
|
||
fail?: (err: { errMsg: string }) => void;
|
||
complete?: () => void;
|
||
};
|
||
|
||
function buildArticleBlock(): BentoBlockFixture {
|
||
return {
|
||
id: 3,
|
||
cardId: 'card-101',
|
||
type: 'article',
|
||
colSpan: 1,
|
||
rowSpan: 1,
|
||
sortOrder: 2,
|
||
title: 'The Future of Cross-Border Trade',
|
||
content: '<p>Cross-border trade is shifting toward trusted digital identity.</p>',
|
||
mediaUrl: 'https://cdn.omnicard.ai/media/article-cover.jpg',
|
||
linkUrl: 'https://omnicard.ai/p/article-3',
|
||
};
|
||
}
|
||
|
||
function buildPortalPayload(blocks: BentoBlockFixture[]) {
|
||
return {
|
||
code: 1000,
|
||
message: 'success',
|
||
data: {
|
||
id: 'card-101',
|
||
name: 'Alex Vance',
|
||
title: 'Managing Director & Global Partner',
|
||
company: 'Nexus Tech Global Ltd',
|
||
status: 'published',
|
||
bentoBlocks: blocks,
|
||
},
|
||
};
|
||
}
|
||
|
||
type ShareWithSystemOption = {
|
||
type?: string;
|
||
summary?: string;
|
||
href?: string;
|
||
success?: () => void;
|
||
fail?: (err: { errCode?: number; errMsg?: string }) => void;
|
||
};
|
||
|
||
describe('OmniCard AI 用户端文章详情业务逻辑测试 (useArticleDetail.uts)', () => {
|
||
let storageMap: Map<string, string>;
|
||
let toastCalls: UniToastOption[];
|
||
let clipboardText: string;
|
||
let clipboardMode: 'success' | 'fail';
|
||
let shareMode: 'success' | 'fail' | 'cancel' | 'missing';
|
||
let shareCalls: ShareWithSystemOption[];
|
||
let lastRequestOptions: UniRequestOption | null;
|
||
let requestCallCount: number;
|
||
let mockRequestResponse: {
|
||
statusCode?: number;
|
||
data?: {
|
||
code?: number;
|
||
message?: string;
|
||
data?: unknown;
|
||
};
|
||
failError?: { errMsg: string };
|
||
};
|
||
|
||
beforeEach(() => {
|
||
storageMap = new Map();
|
||
toastCalls = [];
|
||
clipboardText = '';
|
||
clipboardMode = 'success';
|
||
shareMode = 'success';
|
||
shareCalls = [];
|
||
lastRequestOptions = null;
|
||
requestCallCount = 0;
|
||
mockRequestResponse = {
|
||
statusCode: 200,
|
||
data: buildPortalPayload([
|
||
{
|
||
id: 1,
|
||
cardId: 'card-101',
|
||
type: 'hero',
|
||
colSpan: 2,
|
||
rowSpan: 1,
|
||
sortOrder: 0,
|
||
title: 'Hero Block',
|
||
},
|
||
{
|
||
id: 2,
|
||
cardId: 'card-101',
|
||
type: 'case',
|
||
colSpan: 2,
|
||
rowSpan: 1,
|
||
sortOrder: 1,
|
||
title: 'Middle East Energy Hub',
|
||
content: 'Built scalable renewable energy microgrid in UAE.',
|
||
},
|
||
buildArticleBlock(),
|
||
{
|
||
id: 4,
|
||
cardId: 'card-101',
|
||
type: 'social',
|
||
colSpan: 1,
|
||
rowSpan: 1,
|
||
sortOrder: 3,
|
||
title: 'Global Social Matrix',
|
||
},
|
||
]),
|
||
};
|
||
|
||
const uniMock: Record<string, unknown> = {
|
||
getStorageSync: (key: string) => storageMap.get(key) || '',
|
||
setStorageSync: (key: string, val: string) => storageMap.set(key, val),
|
||
removeStorageSync: (key: string) => storageMap.delete(key),
|
||
getLocale: () => 'zh-CN',
|
||
getSystemInfoSync: () => ({ language: 'zh-CN' }),
|
||
showToast: (options: UniToastOption) => {
|
||
toastCalls.push(options);
|
||
},
|
||
reLaunch: jest.fn(),
|
||
navigateBack: jest.fn(),
|
||
getCurrentPages: () => [],
|
||
setClipboardData: (options: {
|
||
data: string;
|
||
success?: () => void;
|
||
fail?: (err: { errMsg: string }) => void;
|
||
}) => {
|
||
if (clipboardMode === 'fail') {
|
||
options.fail?.({ errMsg: 'setClipboardData:fail' });
|
||
return;
|
||
}
|
||
clipboardText = options.data;
|
||
options.success?.();
|
||
},
|
||
shareWithSystem: (options: ShareWithSystemOption) => {
|
||
shareCalls.push(options);
|
||
if (shareMode === 'missing') {
|
||
options.fail?.({ errCode: 1310602, errMsg: 'share unavailable' });
|
||
return;
|
||
}
|
||
if (shareMode === 'success') {
|
||
options.success?.();
|
||
return;
|
||
}
|
||
if (shareMode === 'cancel') {
|
||
options.fail?.({ errCode: 1310600, errMsg: 'Cancel share' });
|
||
return;
|
||
}
|
||
options.fail?.({ errCode: 1310602, errMsg: 'share fail' });
|
||
},
|
||
request: (options: UniRequestOption) => {
|
||
requestCallCount += 1;
|
||
lastRequestOptions = options;
|
||
setTimeout(() => {
|
||
if (mockRequestResponse.failError) {
|
||
options.fail?.(mockRequestResponse.failError);
|
||
} else {
|
||
options.success?.({
|
||
statusCode: mockRequestResponse.statusCode ?? 200,
|
||
data: mockRequestResponse.data,
|
||
header: {},
|
||
});
|
||
}
|
||
options.complete?.();
|
||
}, 0);
|
||
return {
|
||
abort: jest.fn(),
|
||
};
|
||
},
|
||
};
|
||
|
||
(global as { uni?: Record<string, unknown> }).uni = uniMock;
|
||
});
|
||
|
||
afterEach(() => {
|
||
delete (global as { uni?: Record<string, unknown> }).uni;
|
||
jest.resetModules();
|
||
});
|
||
|
||
function loadUseArticleDetail(): ArticleDetailModule {
|
||
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
||
return require('../../../mpuniapp/pages/portal/useArticleDetail.uts') as ArticleDetailModule;
|
||
}
|
||
|
||
describe('1. 能选出 article 块', () => {
|
||
it('1.1 pickArticleBlock 按 blockId 选出 type 为 article 的块', () => {
|
||
const { pickArticleBlock } = loadUseArticleDetail();
|
||
const article = buildArticleBlock();
|
||
const picked = pickArticleBlock(
|
||
[
|
||
{
|
||
id: 2,
|
||
cardId: 'card-101',
|
||
type: 'case',
|
||
colSpan: 2,
|
||
rowSpan: 1,
|
||
sortOrder: 1,
|
||
title: 'Case',
|
||
},
|
||
article,
|
||
{
|
||
id: 4,
|
||
cardId: 'card-101',
|
||
type: 'social',
|
||
colSpan: 1,
|
||
rowSpan: 1,
|
||
sortOrder: 3,
|
||
title: 'Social',
|
||
},
|
||
],
|
||
'3'
|
||
);
|
||
|
||
expect(picked).not.toBeNull();
|
||
expect(picked?.type).toBe('article');
|
||
expect(String(picked?.id)).toBe('3');
|
||
expect(picked?.title).toBe('The Future of Cross-Border Trade');
|
||
});
|
||
|
||
it('1.2 loadArticleDetail 拉取门户后按 cardId/blockId 绑定文章字段', async () => {
|
||
const {
|
||
createArticleDetailState,
|
||
applyArticleDetailOptions,
|
||
loadArticleDetail,
|
||
} = loadUseArticleDetail();
|
||
|
||
const state = createArticleDetailState();
|
||
const ready = applyArticleDetailOptions(state, {
|
||
cardId: 'card-101',
|
||
blockId: '3',
|
||
});
|
||
expect(ready).toBe(true);
|
||
|
||
const result = await loadArticleDetail(state);
|
||
|
||
expect(result).not.toBeNull();
|
||
expect(requestCallCount).toBe(1);
|
||
expect(lastRequestOptions?.url).toContain('/open/card/portal/detail');
|
||
expect(state.empty).toBe(false);
|
||
expect(state.article?.type).toBe('article');
|
||
expect(state.title).toBe('The Future of Cross-Border Trade');
|
||
expect(state.content).toContain('Cross-border trade');
|
||
expect(state.mediaUrl).toBe('https://cdn.omnicard.ai/media/article-cover.jpg');
|
||
expect(state.linkUrl).toBe('https://omnicard.ai/p/article-3');
|
||
expect(state.isHtmlContent).toBe(true);
|
||
expect(state.openGraph.title).toBe('The Future of Cross-Border Trade');
|
||
expect(state.openGraph.image).toBe('https://cdn.omnicard.ai/media/article-cover.jpg');
|
||
expect(state.openGraph.description.length).toBeGreaterThan(0);
|
||
});
|
||
|
||
it('1.3 正文像 HTML 时 looksLikeHtml 为真,纯文本为假', () => {
|
||
const { looksLikeHtml } = loadUseArticleDetail();
|
||
expect(looksLikeHtml('<p>Insight body</p>')).toBe(true);
|
||
expect(looksLikeHtml('Plain magazine paragraph without markup')).toBe(false);
|
||
});
|
||
});
|
||
|
||
describe('2. 缺失返空', () => {
|
||
it('2.1 缺少 cardId 或 blockId 时不请求并标记空状态', async () => {
|
||
const {
|
||
createArticleDetailState,
|
||
applyArticleDetailOptions,
|
||
loadArticleDetail,
|
||
} = loadUseArticleDetail();
|
||
|
||
const state = createArticleDetailState();
|
||
const ready = applyArticleDetailOptions(state, {});
|
||
expect(ready).toBe(false);
|
||
expect(state.empty).toBe(true);
|
||
|
||
const result = await loadArticleDetail(state);
|
||
expect(result).toBeNull();
|
||
expect(requestCallCount).toBe(0);
|
||
expect(state.empty).toBe(true);
|
||
expect(state.article).toBeNull();
|
||
expect(state.title).toBe('');
|
||
});
|
||
|
||
it('2.2 blockId 命中非 article 或块不存在时返空', async () => {
|
||
const {
|
||
pickArticleBlock,
|
||
createArticleDetailState,
|
||
applyArticleDetailOptions,
|
||
loadArticleDetail,
|
||
} = loadUseArticleDetail();
|
||
|
||
expect(
|
||
pickArticleBlock(
|
||
[
|
||
{
|
||
id: 2,
|
||
cardId: 'card-101',
|
||
type: 'case',
|
||
colSpan: 2,
|
||
rowSpan: 1,
|
||
sortOrder: 1,
|
||
title: 'Case',
|
||
},
|
||
],
|
||
'2'
|
||
)
|
||
).toBeNull();
|
||
expect(pickArticleBlock([], '3')).toBeNull();
|
||
expect(pickArticleBlock(null, '3')).toBeNull();
|
||
|
||
const state = createArticleDetailState();
|
||
applyArticleDetailOptions(state, { cardId: 'card-101', blockId: '2' });
|
||
const result = await loadArticleDetail(state);
|
||
|
||
expect(result).toBeNull();
|
||
expect(state.empty).toBe(true);
|
||
expect(state.article).toBeNull();
|
||
expect(state.openGraph.title).toBe('');
|
||
expect(state.openGraph.description).toBe('');
|
||
expect(state.openGraph.image).toBe('');
|
||
});
|
||
|
||
it('2.3 拉取失败同样返空', async () => {
|
||
const {
|
||
createArticleDetailState,
|
||
applyArticleDetailOptions,
|
||
loadArticleDetail,
|
||
} = loadUseArticleDetail();
|
||
|
||
mockRequestResponse = {
|
||
statusCode: 404,
|
||
data: {
|
||
code: 404,
|
||
message: '名片不存在或未公开',
|
||
},
|
||
};
|
||
|
||
const state = createArticleDetailState();
|
||
applyArticleDetailOptions(state, { cardId: 'missing', blockId: '3' });
|
||
const result = await loadArticleDetail(state);
|
||
|
||
expect(result).toBeNull();
|
||
expect(state.empty).toBe(true);
|
||
expect(state.loading).toBe(false);
|
||
expect(state.article).toBeNull();
|
||
});
|
||
});
|
||
|
||
describe('3. 分享文案字段齐全', () => {
|
||
it('3.1 分享字段包含 CTA、Toast、链接与 OpenGraph title/description/image', async () => {
|
||
const {
|
||
createArticleDetailState,
|
||
applyArticleDetailOptions,
|
||
loadArticleDetail,
|
||
getArticleShareFields,
|
||
resolveArticleDetailText,
|
||
copyArticleShareLink,
|
||
} = loadUseArticleDetail();
|
||
|
||
const state = createArticleDetailState();
|
||
applyArticleDetailOptions(state, { cardId: 'card-101', blockId: '3' });
|
||
await loadArticleDetail(state);
|
||
|
||
const share = getArticleShareFields(state, 'https://omnicard.ai/h5/article');
|
||
const keys: Array<keyof ArticleShareFields> = [
|
||
'ctaLabel',
|
||
'copiedToast',
|
||
'shareUrl',
|
||
'title',
|
||
'description',
|
||
'image',
|
||
];
|
||
for (const key of keys) {
|
||
expect(typeof share[key]).toBe('string');
|
||
expect(share[key].length).toBeGreaterThan(0);
|
||
}
|
||
// 底栏主 CTA 语义:复制分享链接(走 resolve,中英齐全)
|
||
expect(share.ctaLabel).toBe(resolveArticleDetailText('shareCta', 'zh-CN'));
|
||
expect(share.ctaLabel).toBe('复制分享链接');
|
||
expect(resolveArticleDetailText('shareCta', 'en-US')).toBe('Copy share link');
|
||
expect(share.shareUrl).toBe('https://omnicard.ai/p/article-3');
|
||
expect(share.title).toBe('The Future of Cross-Border Trade');
|
||
expect(share.image).toBe('https://cdn.omnicard.ai/media/article-cover.jpg');
|
||
|
||
// 有 linkUrl 时剪贴板复制块链接(不新造域名)
|
||
const copied = copyArticleShareLink(state, 'https://omnicard.ai/h5/article');
|
||
expect(copied).toBe(true);
|
||
expect(clipboardText).toBe('https://omnicard.ai/p/article-3');
|
||
expect(toastCalls.length).toBeGreaterThan(0);
|
||
});
|
||
|
||
it('3.2 无 linkUrl 时回退当前页路径 URL,复制写入剪贴板', async () => {
|
||
const article = buildArticleBlock();
|
||
article.linkUrl = '';
|
||
mockRequestResponse = {
|
||
statusCode: 200,
|
||
data: buildPortalPayload([article]),
|
||
};
|
||
|
||
const {
|
||
createArticleDetailState,
|
||
applyArticleDetailOptions,
|
||
loadArticleDetail,
|
||
resolveShareUrl,
|
||
getArticleShareFields,
|
||
copyArticleShareLink,
|
||
resolveArticleDetailText,
|
||
} = loadUseArticleDetail();
|
||
|
||
expect(resolveShareUrl('', 'https://omnicard.ai/h5/article-fallback')).toBe(
|
||
'https://omnicard.ai/h5/article-fallback'
|
||
);
|
||
expect(resolveShareUrl('https://omnicard.ai/p/article-3', 'https://fallback')).toBe(
|
||
'https://omnicard.ai/p/article-3'
|
||
);
|
||
|
||
const state = createArticleDetailState();
|
||
applyArticleDetailOptions(state, { cardId: 'card-101', blockId: '3' });
|
||
await loadArticleDetail(state);
|
||
|
||
const pageUrl = 'https://omnicard.ai/h5/article-fallback';
|
||
const share = getArticleShareFields(state, pageUrl);
|
||
expect(share.shareUrl).toBe(pageUrl);
|
||
expect(share.ctaLabel).toBe(resolveArticleDetailText('shareCta'));
|
||
expect(share.ctaLabel).toBe('复制分享链接');
|
||
expect(share.copiedToast.length).toBeGreaterThan(0);
|
||
|
||
const copied = copyArticleShareLink(state, pageUrl);
|
||
expect(copied).toBe(true);
|
||
expect(clipboardText).toBe(pageUrl);
|
||
expect(toastCalls.length).toBeGreaterThan(0);
|
||
});
|
||
});
|
||
|
||
describe('4. 专栏作者微展台与核心摘要导读', () => {
|
||
it('4.1 加载文章时能一并解析名片作者信息与核心摘要', async () => {
|
||
const {
|
||
createArticleDetailState,
|
||
applyArticleDetailOptions,
|
||
loadArticleDetail,
|
||
} = loadUseArticleDetail();
|
||
|
||
const state = createArticleDetailState();
|
||
applyArticleDetailOptions(state, { cardId: 'card-101', blockId: '3' });
|
||
await loadArticleDetail(state);
|
||
|
||
expect(state.authorName).toBe('Alex Vance');
|
||
expect(state.authorTitle).toBe('Managing Director & Global Partner');
|
||
expect(state.authorCompany).toBe('Nexus Tech Global Ltd');
|
||
expect(state.readTimeMinutes).toBeGreaterThanOrEqual(1);
|
||
expect(state.summaryText?.length).toBeGreaterThan(0);
|
||
});
|
||
});
|
||
|
||
describe('5. 阅读滑屏深度计算与雷达停留时长上报', () => {
|
||
it('5.1 updateArticleScrollDepth 正确计算并单调递增滑屏百分比', () => {
|
||
const { createArticleDetailState, updateArticleScrollDepth } = loadUseArticleDetail();
|
||
const state = createArticleDetailState();
|
||
|
||
expect(state.scrollDepth).toBe(0);
|
||
// 顶部
|
||
updateArticleScrollDepth(state, 0, 2000, 600);
|
||
expect(state.scrollDepth).toBe(0);
|
||
|
||
// 中间 (scrollTop = 700 / maxScroll = 1400 => 50%)
|
||
updateArticleScrollDepth(state, 700, 2000, 600);
|
||
expect(state.scrollDepth).toBe(50);
|
||
|
||
// 向上回滚时保持最大深度
|
||
updateArticleScrollDepth(state, 200, 2000, 600);
|
||
expect(state.scrollDepth).toBe(50);
|
||
|
||
// 触底 (100%)
|
||
updateArticleScrollDepth(state, 1400, 2000, 600);
|
||
expect(state.scrollDepth).toBe(100);
|
||
});
|
||
|
||
it('5.2 reportArticleStay 将停留时长与阅读深度正确上报至雷达', async () => {
|
||
const {
|
||
createArticleDetailState,
|
||
applyArticleDetailOptions,
|
||
loadArticleDetail,
|
||
reportArticleStay,
|
||
} = loadUseArticleDetail();
|
||
|
||
const state = createArticleDetailState();
|
||
applyArticleDetailOptions(state, { cardId: 'card-101', blockId: '3' });
|
||
await loadArticleDetail(state);
|
||
|
||
mockRequestResponse = {
|
||
statusCode: 200,
|
||
data: {
|
||
code: 1000,
|
||
message: 'success',
|
||
data: {
|
||
id: 'track-1',
|
||
cardId: 'card-101',
|
||
visitorId: 'vis-test',
|
||
touchType: 'link',
|
||
stayDurationMs: 1200,
|
||
scrollDepth: 75,
|
||
viewedBlocks: ['3'],
|
||
},
|
||
},
|
||
};
|
||
|
||
state.enterTime = Date.now() - 1500;
|
||
state.scrollDepth = 75;
|
||
|
||
await reportArticleStay(state, 75);
|
||
|
||
expect(lastRequestOptions?.url).toContain('/open/crm/radar/track');
|
||
const body = lastRequestOptions?.data as Record<string, unknown>;
|
||
expect(body.cardId).toBe('card-101');
|
||
expect(body.touchType).toBe('link');
|
||
expect(body.scrollDepth).toBe(75);
|
||
expect(body.viewedBlocks).toEqual(['3']);
|
||
});
|
||
});
|
||
|
||
describe('6. 多语言本地化文案体系', () => {
|
||
it('6.1 覆盖 zh-CN, en-US, ar-SA, es-ES 等语种的核心文案', () => {
|
||
const { resolveArticleDetailText } = loadUseArticleDetail();
|
||
|
||
expect(resolveArticleDetailText('kicker', 'zh-CN')).toBe('深度洞见');
|
||
expect(resolveArticleDetailText('summary', 'zh-CN')).toBe('核心导读');
|
||
expect(resolveArticleDetailText('author', 'zh-CN')).toBe('专栏作者');
|
||
expect(resolveArticleDetailText('viewPortal', 'zh-CN')).toBe('查看微官网');
|
||
expect(resolveArticleDetailText('shareCta', 'zh-CN')).toBe('复制分享链接');
|
||
|
||
expect(resolveArticleDetailText('kicker', 'en-US')).toBe('Thought Leadership');
|
||
expect(resolveArticleDetailText('summary', 'en-US')).toBe('Key Takeaways');
|
||
expect(resolveArticleDetailText('author', 'en-US')).toBe('Author');
|
||
expect(resolveArticleDetailText('viewPortal', 'en-US')).toBe('View Profile');
|
||
expect(resolveArticleDetailText('shareCta', 'en-US')).toBe('Copy share link');
|
||
|
||
expect(resolveArticleDetailText('kicker', 'ar-SA')).toBe('رؤى متعمقة');
|
||
expect(resolveArticleDetailText('summary', 'ar-SA')).toBe('أبرز النقاط');
|
||
|
||
expect(resolveArticleDetailText('kicker', 'es-ES')).toBe('Perspectivas');
|
||
expect(resolveArticleDetailText('summary', 'es-ES')).toBe('Puntos clave');
|
||
});
|
||
});
|
||
|
||
describe('7. R50 系统分享仅成功才提示已分享', () => {
|
||
async function loadArticleState(): Promise<ArticleDetailState> {
|
||
const {
|
||
createArticleDetailState,
|
||
applyArticleDetailOptions,
|
||
loadArticleDetail,
|
||
} = loadUseArticleDetail();
|
||
const state = createArticleDetailState();
|
||
applyArticleDetailOptions(state, { cardId: 'card-101', blockId: '3' });
|
||
await loadArticleDetail(state);
|
||
return state;
|
||
}
|
||
|
||
it('7.1 shareWithSystem 明确 success 才 toast 已分享', async () => {
|
||
const { shareArticleViaSystem, resolveArticleDetailText } = loadUseArticleDetail();
|
||
const state = await loadArticleState();
|
||
toastCalls = [];
|
||
shareMode = 'success';
|
||
|
||
const ok = await shareArticleViaSystem(state, 'https://omnicard.ai/h5/article');
|
||
|
||
expect(ok).toBe(true);
|
||
expect(shareCalls.length).toBe(1);
|
||
expect(shareCalls[0].href).toBe('https://omnicard.ai/p/article-3');
|
||
expect(toastCalls.some(t => t.title === resolveArticleDetailText('shareSuccess'))).toBe(
|
||
true
|
||
);
|
||
expect(resolveArticleDetailText('shareSuccess', 'zh-CN')).toBe('已分享');
|
||
});
|
||
|
||
it('7.2 系统分享 API 失败不 toast 已分享', async () => {
|
||
const { shareArticleViaSystem } = loadUseArticleDetail();
|
||
const state = await loadArticleState();
|
||
toastCalls = [];
|
||
shareMode = 'fail';
|
||
|
||
const ok = await shareArticleViaSystem(state, 'https://omnicard.ai/h5/article');
|
||
|
||
expect(ok).toBe(false);
|
||
expect(shareCalls.length).toBe(1);
|
||
assertNoShareSuccessToast(toastCalls);
|
||
});
|
||
|
||
it('7.3 用户取消分享不 toast 已分享', async () => {
|
||
const { shareArticleViaSystem } = loadUseArticleDetail();
|
||
const state = await loadArticleState();
|
||
toastCalls = [];
|
||
shareMode = 'cancel';
|
||
|
||
const ok = await shareArticleViaSystem(state, 'https://omnicard.ai/h5/article');
|
||
|
||
expect(ok).toBe(false);
|
||
expect(shareCalls.length).toBe(1);
|
||
assertNoShareSuccessToast(toastCalls);
|
||
});
|
||
|
||
it('7.4 系统分享 API 不存在时不 toast 已分享', async () => {
|
||
const uniRef = (global as { uni?: Record<string, unknown> }).uni;
|
||
if (uniRef != null) {
|
||
delete uniRef.shareWithSystem;
|
||
}
|
||
const { shareArticleViaSystem } = loadUseArticleDetail();
|
||
const state = await loadArticleState();
|
||
toastCalls = [];
|
||
shareCalls = [];
|
||
|
||
const ok = await shareArticleViaSystem(state, 'https://omnicard.ai/h5/article');
|
||
|
||
expect(ok).toBe(false);
|
||
expect(shareCalls.length).toBe(0);
|
||
assertNoShareSuccessToast(toastCalls);
|
||
});
|
||
|
||
it('7.5 空分享链接不调用分享 API,不 toast 已分享', async () => {
|
||
const { createArticleDetailState, shareArticleViaSystem } = loadUseArticleDetail();
|
||
const state = createArticleDetailState();
|
||
state.linkUrl = '';
|
||
toastCalls = [];
|
||
shareCalls = [];
|
||
|
||
const ok = await shareArticleViaSystem(state, '');
|
||
|
||
expect(ok).toBe(false);
|
||
expect(shareCalls.length).toBe(0);
|
||
assertNoShareSuccessToast(toastCalls);
|
||
});
|
||
|
||
it('7.6 复制链接失败不说成已分享;成功仍用直链已复制', async () => {
|
||
const {
|
||
copyArticleShareLink,
|
||
getArticleShareFields,
|
||
resolveArticleDetailText,
|
||
} = loadUseArticleDetail();
|
||
const state = await loadArticleState();
|
||
|
||
// 复制失败
|
||
clipboardMode = 'fail';
|
||
toastCalls = [];
|
||
const failed = copyArticleShareLink(state, 'https://omnicard.ai/h5/article');
|
||
expect(failed).toBe(true);
|
||
assertNoShareSuccessToast(toastCalls);
|
||
expect(
|
||
toastCalls.some(t => t.title === resolveArticleDetailText('shareSuccess'))
|
||
).toBe(false);
|
||
|
||
// P22:复制成功提示保持不变
|
||
clipboardMode = 'success';
|
||
toastCalls = [];
|
||
const share = getArticleShareFields(state, 'https://omnicard.ai/h5/article');
|
||
const ok = copyArticleShareLink(state, 'https://omnicard.ai/h5/article');
|
||
expect(ok).toBe(true);
|
||
expect(toastCalls.some(t => t.title === share.copiedToast)).toBe(true);
|
||
expect(toastCalls.some(t => t.title === '直链已复制')).toBe(true);
|
||
assertNoShareSuccessToast(toastCalls);
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('OmniCard AI 文章详情页分享诚实反馈 (article-detail.uvue)', () => {
|
||
const fs = require('fs') as typeof import('fs');
|
||
const path = require('path') as typeof import('path');
|
||
const ROOT = path.resolve(__dirname, '../../../mpuniapp/pages/portal');
|
||
const PAGE = path.join(ROOT, 'article-detail.uvue');
|
||
const UTS = path.join(ROOT, 'useArticleDetail.uts');
|
||
|
||
it('系统分享成功 toast 仅出现在 shareWithSystem success 回调;失败路径无已分享', () => {
|
||
const pageSource = fs.readFileSync(PAGE, 'utf8');
|
||
const logicSource = fs.readFileSync(UTS, 'utf8');
|
||
|
||
expect(logicSource.includes('shareArticleViaSystem')).toBe(true);
|
||
expect(logicSource.includes('shareWithSystem')).toBe(true);
|
||
expect(
|
||
pageSource.includes('shareArticleViaSystem') ||
|
||
pageSource.includes('performArticleShareAction')
|
||
).toBe(true);
|
||
|
||
const successKeyIdx = logicSource.indexOf("resolveArticleDetailText('shareSuccess')");
|
||
expect(successKeyIdx).toBeGreaterThan(-1);
|
||
|
||
const shareApiIdx = logicSource.indexOf('shareWithSystem');
|
||
expect(shareApiIdx).toBeGreaterThan(-1);
|
||
expect(successKeyIdx).toBeGreaterThan(shareApiIdx);
|
||
|
||
// fail 分支不得含已分享
|
||
const failIdx = logicSource.indexOf('fail:', shareApiIdx);
|
||
expect(failIdx).toBeGreaterThan(successKeyIdx);
|
||
const failBody = logicSource.slice(failIdx, failIdx + 280);
|
||
expect(failBody.includes('shareSuccess')).toBe(false);
|
||
expect(SHARE_SUCCESS_TOAST_RE.test(failBody)).toBe(false);
|
||
|
||
// 复制失败分支也不得已分享
|
||
const clipIdx = logicSource.indexOf('setClipboardData');
|
||
expect(clipIdx).toBeGreaterThan(-1);
|
||
const clipFailIdx = logicSource.indexOf('fail:', clipIdx);
|
||
expect(clipFailIdx).toBeGreaterThan(clipIdx);
|
||
const clipFailBody = logicSource.slice(clipFailIdx, clipFailIdx + 200);
|
||
expect(clipFailBody.includes('shareSuccess')).toBe(false);
|
||
expect(SHARE_SUCCESS_TOAST_RE.test(clipFailBody)).toBe(false);
|
||
|
||
// P22 复制成功仍用 portal.copied_link
|
||
expect(logicSource.includes("portal.copied_link")).toBe(true);
|
||
});
|
||
});
|