/** * 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; copyArticleShareLink: (state: ArticleDetailState, pageUrl?: string) => boolean; shareArticleViaSystem: ( state: ArticleDetailState, pageUrl?: string ) => Promise; resolveArticleDetailText: (key: string, locale?: string) => string; updateArticleScrollDepth: ( state: ArticleDetailState, scrollTop: number, scrollHeight: number, windowHeight: number ) => number; reportArticleStay: (state: ArticleDetailState, scrollDepth?: number) => Promise; 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; }; 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: '

Cross-border trade is shifting toward trusted digital identity.

', 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; 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 = { 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 }).uni = uniMock; }); afterEach(() => { delete (global as { uni?: Record }).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('

Insight body

')).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 = [ '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; 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 { 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 }).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); }); });