216 lines
7.6 KiB
TypeScript
216 lines
7.6 KiB
TypeScript
import { describe, expect, it } from 'vitest';
|
||
import {
|
||
getVideoExportCoverLabels,
|
||
resolveVideoExportCta,
|
||
} from '@/lib/video-export-app/cover-config';
|
||
import type { Locale } from '@/lib/i18n';
|
||
|
||
const BIDI_CONTROL_CASES = [
|
||
['U+061C', '\u061c', '%D8%9C'],
|
||
['U+200E', '\u200e', '%E2%80%8E'],
|
||
['U+200F', '\u200f', '%E2%80%8F'],
|
||
['U+202A', '\u202a', '%E2%80%AA'],
|
||
['U+202B', '\u202b', '%E2%80%AB'],
|
||
['U+202C', '\u202c', '%E2%80%AC'],
|
||
['U+202D', '\u202d', '%E2%80%AD'],
|
||
['U+202E', '\u202e', '%E2%80%AE'],
|
||
['U+2066', '\u2066', '%E2%81%A6'],
|
||
['U+2067', '\u2067', '%E2%81%A7'],
|
||
['U+2068', '\u2068', '%E2%81%A8'],
|
||
['U+2069', '\u2069', '%E2%81%A9'],
|
||
] as const;
|
||
|
||
describe('resolveVideoExportCta', () => {
|
||
it.each([undefined, '', ' '])('defaults an absent or blank value (%j)', (raw) => {
|
||
expect(resolveVideoExportCta(raw)).toEqual({ destination: 'open.maic.chat' });
|
||
});
|
||
|
||
it.each(['off', 'OFF', ' Off '])('disables the CTA for %j', (raw) => {
|
||
expect(resolveVideoExportCta(raw)).toBeNull();
|
||
});
|
||
|
||
it.each([
|
||
['https://Example.COM/learn/', 'example.com/learn'],
|
||
['http://open.maic.chat/', 'open.maic.chat'],
|
||
['courses.example.com/start/', 'courses.example.com/start'],
|
||
])('normalizes %j to the display destination %j', (raw, destination) => {
|
||
expect(resolveVideoExportCta(raw)).toEqual({ destination });
|
||
});
|
||
|
||
it.each([
|
||
'ftp://example.com',
|
||
'javascript:alert(1)',
|
||
'https://user:pass@example.com',
|
||
'https://@example.com',
|
||
'https://example.com/path?source=video',
|
||
'https://example.com/path?',
|
||
'https://example.com/path#section',
|
||
'https://example.com/path#',
|
||
'example.com/path?source=video',
|
||
'example.com/path#section',
|
||
'example.com/\u0000path',
|
||
])('rejects unsafe destination %j', (raw) => {
|
||
expect(resolveVideoExportCta(raw)).toBeNull();
|
||
});
|
||
|
||
it('accepts 96 raw characters and rejects 97', () => {
|
||
const at96 = `example.com/${'a'.repeat(84)}`;
|
||
const at97 = `${at96}a`;
|
||
|
||
expect(at96).toHaveLength(96);
|
||
expect(at97).toHaveLength(97);
|
||
expect(resolveVideoExportCta(at96)).toEqual({ destination: at96 });
|
||
expect(resolveVideoExportCta(at97)).toBeNull();
|
||
});
|
||
|
||
it('keeps an accepted 96-character Unicode path human-readable and bounded', () => {
|
||
const at96 = `a.co/${'学'.repeat(91)}`;
|
||
|
||
expect(at96).toHaveLength(96);
|
||
expect(resolveVideoExportCta(at96)).toEqual({ destination: at96 });
|
||
expect(resolveVideoExportCta(at96)?.destination).toHaveLength(96);
|
||
});
|
||
|
||
it('preserves a Unicode IDN while lowercasing only ASCII host letters', () => {
|
||
expect(resolveVideoExportCta('https://例え.Example/学ぶ/')).toEqual({
|
||
destination: '例え.example/学ぶ',
|
||
});
|
||
expect(resolveVideoExportCta('https://İ.Example/Path')).toEqual({
|
||
destination: 'İ.example/Path',
|
||
});
|
||
});
|
||
|
||
it('decodes an unambiguous percent-encoded path for display', () => {
|
||
expect(resolveVideoExportCta('https://Example.COM/%E5%AD%A6%E3%81%B6/')).toEqual({
|
||
destination: 'example.com/学ぶ',
|
||
});
|
||
});
|
||
|
||
it('normalizes ASCII host casing and trailing slashes without changing path casing', () => {
|
||
expect(resolveVideoExportCta('HTTPS://Example.COM/LEARN///')).toEqual({
|
||
destination: 'example.com/LEARN',
|
||
});
|
||
});
|
||
|
||
it.each([
|
||
['https://Example.COM/a%2Fb', 'example.com/a%2Fb'],
|
||
['https://Example.COM/a%5Cb', 'example.com/a%5Cb'],
|
||
['https://Example.COM/a%3Fb', 'example.com/a%3Fb'],
|
||
['https://Example.COM/a%23b', 'example.com/a%23b'],
|
||
['https://Example.COM/a%25b', 'example.com/a%25b'],
|
||
])('preserves an encoded HTTP path delimiter in %j', (raw, destination) => {
|
||
expect(resolveVideoExportCta(raw)).toEqual({ destination });
|
||
});
|
||
|
||
it.each(['https://example.com/%ZZ', 'https://example.com\\different-target'])(
|
||
'rejects ambiguous or malformed display destination %j',
|
||
(raw) => {
|
||
expect(resolveVideoExportCta(raw)).toBeNull();
|
||
},
|
||
);
|
||
|
||
it.each(BIDI_CONTROL_CASES)('rejects raw bidi control %s in the host', (_, raw) => {
|
||
expect(resolveVideoExportCta(`https://safe${raw}.example/learn`)).toBeNull();
|
||
});
|
||
|
||
it.each(BIDI_CONTROL_CASES)('rejects raw bidi control %s in the path', (_, raw) => {
|
||
expect(resolveVideoExportCta(`https://safe.example/a${raw}b`)).toBeNull();
|
||
});
|
||
|
||
it.each(BIDI_CONTROL_CASES)(
|
||
'rejects percent-encoded bidi control %s in the host',
|
||
(_, __, encoded) => {
|
||
expect(resolveVideoExportCta(`https://safe${encoded}.example/learn`)).toBeNull();
|
||
},
|
||
);
|
||
|
||
it.each(BIDI_CONTROL_CASES)(
|
||
'rejects percent-encoded bidi control %s in the path',
|
||
(_, __, encoded) => {
|
||
expect(resolveVideoExportCta(`https://safe.example/a${encoded}b`)).toBeNull();
|
||
},
|
||
);
|
||
|
||
it('preserves raw and percent-encoded ZWJ emoji sequences', () => {
|
||
expect(resolveVideoExportCta('https://example.com/👩💻')).toEqual({
|
||
destination: 'example.com/👩💻',
|
||
});
|
||
expect(resolveVideoExportCta('https://example.com/%F0%9F%91%A9%E2%80%8D%F0%9F%92%BB')).toEqual({
|
||
destination: 'example.com/👩💻',
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('getVideoExportCoverLabels', () => {
|
||
const expectedCtaLabels: Record<
|
||
Locale,
|
||
Pick<
|
||
ReturnType<typeof getVideoExportCoverLabels>,
|
||
'quizCtaPrompt' | 'pblCtaPrompt' | 'ctaVisit'
|
||
>
|
||
> = {
|
||
'en-US': {
|
||
quizCtaPrompt: 'Want to try an interactive quiz?',
|
||
pblCtaPrompt: 'Want to explore project-based learning?',
|
||
ctaVisit: 'Visit',
|
||
},
|
||
'zh-CN': {
|
||
quizCtaPrompt: '想亲自体验互动测验?',
|
||
pblCtaPrompt: '想亲自参与项目学习?',
|
||
ctaVisit: '访问',
|
||
},
|
||
'zh-TW': {
|
||
quizCtaPrompt: '想親自體驗互動測驗?',
|
||
pblCtaPrompt: '想親自參與專題式學習?',
|
||
ctaVisit: '前往',
|
||
},
|
||
'ja-JP': {
|
||
quizCtaPrompt: 'インタラクティブなクイズを体験しませんか?',
|
||
pblCtaPrompt: 'プロジェクト型学習を体験しませんか?',
|
||
ctaVisit: 'アクセス',
|
||
},
|
||
'ko-KR': {
|
||
quizCtaPrompt: '인터랙티브 퀴즈를 직접 체험해 보세요',
|
||
pblCtaPrompt: '프로젝트 기반 학습을 직접 체험해 보세요',
|
||
ctaVisit: '방문',
|
||
},
|
||
'es-MX': {
|
||
quizCtaPrompt: '¿Quieres probar un cuestionario interactivo?',
|
||
pblCtaPrompt: '¿Quieres explorar el aprendizaje basado en proyectos?',
|
||
ctaVisit: 'Visita',
|
||
},
|
||
'fr-FR': {
|
||
quizCtaPrompt: 'Envie d’essayer un quiz interactif ?',
|
||
pblCtaPrompt: 'Envie d’explorer l’apprentissage par projet ?',
|
||
ctaVisit: 'Découvrir',
|
||
},
|
||
'vi-VN': {
|
||
quizCtaPrompt: 'Muốn thử một bài trắc nghiệm tương tác?',
|
||
pblCtaPrompt: 'Muốn khám phá học tập theo dự án?',
|
||
ctaVisit: 'Ghé thăm',
|
||
},
|
||
'pt-BR': {
|
||
quizCtaPrompt: 'Quer experimentar um quiz interativo?',
|
||
pblCtaPrompt: 'Quer explorar a aprendizagem baseada em projetos?',
|
||
ctaVisit: 'Acesse',
|
||
},
|
||
'ru-RU': {
|
||
quizCtaPrompt: 'Хотите пройти интерактивный тест?',
|
||
pblCtaPrompt: 'Хотите попробовать проектное обучение?',
|
||
ctaVisit: 'Посетите',
|
||
},
|
||
'ar-SA': {
|
||
quizCtaPrompt: 'هل تريد تجربة اختبار تفاعلي؟',
|
||
pblCtaPrompt: 'هل تريد استكشاف التعلم القائم على المشاريع؟',
|
||
ctaVisit: 'تفضل بزيارة',
|
||
},
|
||
};
|
||
|
||
it.each(Object.entries(expectedCtaLabels) as Array<[Locale, (typeof expectedCtaLabels)[Locale]]>)(
|
||
'returns the approved CTA strings for %s',
|
||
(locale, expected) => {
|
||
expect(getVideoExportCoverLabels(locale)).toMatchObject(expected);
|
||
},
|
||
);
|
||
});
|