ちょっと必要だったので便利関数を作った。
要件的に簡単な処理であったし、丁度良かったので初めてJestを使ってみた。
chatGPTにある程度やらせてcopilot使いながら手直し、テスト書かせて手直しという感じ。
最近はコードを0から書くことが全く無くなったがまだまだ使いこなせていない気もする。
目次
Twitter関連の関数
utils/twitter.ts
export const isTwitterUrl = (url: string) => {
const twitterHostnames = [
'twitter.com',
'www.twitter.com',
'mobile.twitter.com',
// 'pbs.twimg.com',
// 't.co',
];
try {
const parser = new URL(url);
return twitterHostnames.includes(parser.hostname);
} catch (error) {
return false;
}
};
export const isTweetUrl = (url: string) => {
if (!isTwitterUrl(url)) return false;
const regex =
/^https?:\/\/(?:www\.|mobile\.)?twitter\.com\/(?:#!\/)?([a-zA-Z0-9_]{1,15})\/status(?:es)?\/(\d+)(?:\?.*)?$/;
return regex.test(url);
};
export const getTweetId = (url: string) => {
if (!isTweetUrl(url)) return null;
const regexes = [
/https?:\/\/twitter\.com\/(\w+)\/status(es)?\/(\d+)/,
/https?:\/\/twitter\.com\/(?:#!\/)?(\w+)\/status(es)?\/(\d+)/,
/https?:\/\/mobile\.twitter\.com\/(\w+)\/status(es)?\/(\d+)/,
];
for (const regex of regexes) {
const match = url.match(regex);
if (match) {
return match[match.length - 1];
}
}
return null;
};
Twitter関連の関数のテスト
utils/__tests__/twitter.test.ts
import { isTwitterUrl, isTweetUrl, getTweetId } from '../twitter';
describe('isTwitterUrl', () => {
test('returns true for valid twitter URLs', () => {
expect(isTwitterUrl('https://twitter.com/')).toBe(true);
expect(isTwitterUrl('https://twitter.com/hashtag/tbt')).toBe(true);
expect(isTwitterUrl('https://twitter.com/search?q=hello')).toBe(true);
expect(isTwitterUrl('https://twitter.com/Twitter/status/1234567890')).toBe(true);
expect(isTwitterUrl('https://www.twitter.com/Twitter/status/1234567890')).toBe(true);
expect(isTwitterUrl('https://mobile.twitter.com/Twitter/status/1234567890')).toBe(true);
});
test('returns false for invalid twitter URLs', () => {
expect(isTwitterUrl('https://pbs.twimg.com/profile_images/1234567890/photo.jpg')).toBe(false);
expect(isTwitterUrl('https://t.co/abcdefg')).toBe(false);
expect(isTwitterUrl('https://www.google.com/')).toBe(false);
});
});
describe('isTweetUrl', () => {
test('valid tweet url', () => {
const urls = [
'https://twitter.com/elonmusk/status/1367528057626767874',
'https://www.twitter.com/elonmusk/status/1367528057626767874',
'https://mobile.twitter.com/elonmusk/status/1367528057626767874',
'https://twitter.com/elonmusk/statuses/1367528057626767874',
'https://www.twitter.com/elonmusk/statuses/1367528057626767874',
'https://mobile.twitter.com/elonmusk/statuses/1367528057626767874',
];
urls.forEach((url) => {
expect(isTweetUrl(url)).toBe(true);
});
});
test('invalid tweet url', () => {
const urls = [
'https://twitter.com/',
'https://twitter.com/elonmusk',
'https://twitter.com/elonmusk/status/',
'https://twitter.com/elonmusk/status/abc',
'https://www.google.com/search?q=elon+musk+twitter&oq=elon+musk+twitter',
];
urls.forEach((url) => {
expect(isTweetUrl(url)).toBe(false);
});
});
});
describe('getTweetId', () => {
it('returns null for invalid URL', () => {
expect(getTweetId('not a URL')).toBeNull();
expect(getTweetId('https://example.com')).toBeNull();
expect(getTweetId('https://twitter.com/')).toBeNull();
expect(getTweetId('https://twitter.com/user')).toBeNull();
expect(getTweetId('https://twitter.com/user/status')).toBeNull();
expect(getTweetId('https://twitter.com/user/status/invalidid')).toBeNull();
});
it('returns the tweet ID for valid URLs', () => {
expect(getTweetId('https://twitter.com/user/status/1234567890')).toBe('1234567890');
expect(getTweetId('https://twitter.com/user/statuses/1234567890')).toBe('1234567890');
expect(getTweetId('https://twitter.com/user/status/1234567890?ref_src=twsrc%5Etfw')).toBe(
'1234567890'
);
expect(getTweetId('https://mobile.twitter.com/user/status/1234567890')).toBe('1234567890');
});
});
Youtube関連の関数
utils/youtube.ts
export const isYouTubeUrl = (url: string) => {
try {
const parser = new URL(url);
return parser.hostname === 'www.youtube.com' || parser.hostname === 'youtu.be';
} catch (error) {
return false;
}
};
export const isYouTubeVideoUrl = (url: string) => {
if (!isYouTubeUrl(url)) return false;
const parser = new URL(url);
return (
parser.pathname.startsWith('/watch') ||
parser.pathname.startsWith('/embed') ||
(parser.hostname === 'youtu.be' && !!parser.pathname.match(/^\/[0-9A-Za-z_-]{10,}$/))
);
};
export const getVideoId = (url: string) => {
if (!isYouTubeVideoUrl(url)) return null;
const regex = /(?:\/|v=)([0-9A-Za-z_-]{10,})+(?:[%#?&]|$)/;
const match = url.match(regex);
return match ? match[1] : null;
};
export const getThumbnailUrl = (youtubeVideoId: string) => {
return `https://img.youtube.com/vi/${youtubeVideoId}/maxresdefault.jpg`;
};
Youtube関連の関数のテスト
utils/__tests__/youtube.test.ts
import { isYouTubeUrl, isYouTubeVideoUrl, getVideoId } from '../youtube';
// 参考
// https://takashiski.hatenablog.com/entry/2021/09/19/124500
describe('isYouTubeUrl', () => {
test('should return true for www.youtube.com', () => {
expect(isYouTubeUrl('https://www.youtube.com/watch?v=dQw4w9WgXcQ')).toBe(true);
expect(isYouTubeUrl('https://www.youtube.com/embed/dQw4w9WgXcQ')).toBe(true);
expect(isYouTubeUrl('https://www.youtube.com/channel/UC-lHJZR3Gqxm24_Vd_AJ5Yw')).toBe(true);
});
test('should return true for youtu.be', () => {
expect(isYouTubeUrl('https://youtu.be/dQw4w9WgXcQ')).toBe(true);
});
test('should return false for other URLs', () => {
expect(isYouTubeUrl('https://www.google.com/')).toBe(false);
});
});
describe('isYouTubeVideoUrl', () => {
test('returns true for valid YouTube video URLs', () => {
const validUrls = [
'https://www.youtube.com/watch?v=dQw4w9WgXcQ',
'https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=10s',
'https://www.youtube.com/embed/dQw4w9WgXcQ',
'https://www.youtube.com/embed/dQw4w9WgXcQ?start=10',
'https://youtu.be/dQw4w9WgXcQ',
'https://www.youtube.com/watch?v=ngNhpdaT5V0&list=PLxai42gkPeMN1nTZFD4PEc96sMI8mdT03&index=3',
];
validUrls.forEach((url) => {
expect(isYouTubeVideoUrl(url)).toBe(true);
});
});
test('returns false for invalid YouTube video URLs', () => {
const invalidUrls = [
'https://www.youtube.com/channel/UC_x5XG1OV2P6uZZ5FSM9Ttw',
'https://www.youtube.com/results?search_query=cats',
'https://www.youtube.com/',
'https://youtu.be/',
];
invalidUrls.forEach((url) => {
expect(isYouTubeVideoUrl(url)).toBe(false);
});
});
test('returns false for non-YouTube URLs', () => {
const nonYouTubeUrls = ['https://example.com', 'https://www.google.com/search?q=youtube'];
nonYouTubeUrls.forEach((url) => {
expect(isYouTubeVideoUrl(url)).toBe(false);
});
});
});
describe('getVideoId', () => {
test('returns video ID for valid YouTube video URLs', () => {
const urlsAndIds = [
['https://www.youtube.com/watch?v=dQw4w9WgXcQ', 'dQw4w9WgXcQ'],
['https://youtu.be/dQw4w9WgXcQ', 'dQw4w9WgXcQ'],
['https://youtu.be/dQw4w9WgXcQ?t=10s', 'dQw4w9WgXcQ'],
['https://www.youtube.com/watch?v=dQw4w9WgXcQ&t=10s', 'dQw4w9WgXcQ'],
['https://www.youtube.com/embed/dQw4w9WgXcQ', 'dQw4w9WgXcQ'],
['https://www.youtube.com/embed/dQw4w9WgXcQ?start=10', 'dQw4w9WgXcQ'],
[
'https://www.youtube.com/watch?v=ngNhpdaT5V0&list=PLxai42gkPeMN1nTZFD4PEc96sMI8mdT03&index=3',
'ngNhpdaT5V0',
],
];
urlsAndIds.forEach(([url, expectedId]) => {
expect(getVideoId(url)).toBe(expectedId);
});
});
test('returns null for invalid or non-YouTube URLs', () => {
const invalidUrls = [
'https://www.youtube.com/channel/UC_x5XG1OV2P6uZZ5FSM9Ttw',
'https://www.youtube.com/results?search_query=cats',
'https://www.youtube.com/',
'https://youtu.be/',
'https://example.com',
'https://www.google.com/search?q=youtube',
];
invalidUrls.forEach((url) => {
expect(getVideoId(url)).toBe(null);
});
});
});