feat: 図解コメントツールを最新版に置換
Made-with: Cursor
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
import { describe, test, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { slugify, generateId } from '../src/shared/slug';
|
||||
import { fmtTime } from '../src/shared/time';
|
||||
import { PRIORITY_CYCLE, PRIORITY_COLORS, HIGHLIGHT_COLORS } from '../src/shared/constants';
|
||||
import { authHeaders } from '../src/shared/api-client';
|
||||
import { mapNormToOrig } from '../src/widget/highlight';
|
||||
|
||||
describe('slugify', () => {
|
||||
test('HTTP プレフィックスを除去する', () => {
|
||||
expect(slugify('https://example.com/page')).toBe('example_com_page');
|
||||
});
|
||||
|
||||
test('日本語文字を保持する', () => {
|
||||
const result = slugify('https://example.com/テスト');
|
||||
expect(result).toContain('テスト');
|
||||
});
|
||||
|
||||
test('100文字で切り詰める', () => {
|
||||
const long = 'https://example.com/' + 'a'.repeat(200);
|
||||
expect(slugify(long).length).toBe(100);
|
||||
});
|
||||
|
||||
test('記号をアンダースコアに変換する', () => {
|
||||
expect(slugify('https://a.b/c?d=e&f=g')).toBe('a_b_c_d_e_f_g');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateId', () => {
|
||||
test('文字列を返す', () => {
|
||||
expect(typeof generateId()).toBe('string');
|
||||
});
|
||||
|
||||
test('一意性がある(100回生成してユニーク)', () => {
|
||||
const ids = new Set(Array.from({ length: 100 }, () => generateId()));
|
||||
expect(ids.size).toBe(100);
|
||||
});
|
||||
});
|
||||
|
||||
describe('fmtTime', () => {
|
||||
let realNow: () => number;
|
||||
|
||||
beforeEach(() => {
|
||||
realNow = Date.now;
|
||||
vi.spyOn(Date, 'now').mockReturnValue(1700000000000);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
test('1分未満は「たった今」', () => {
|
||||
expect(fmtTime(1700000000000 - 30000)).toBe('たった今');
|
||||
});
|
||||
|
||||
test('1時間未満は「N分前」', () => {
|
||||
expect(fmtTime(1700000000000 - 300000)).toBe('5分前');
|
||||
});
|
||||
|
||||
test('24時間未満は「N時間前」', () => {
|
||||
expect(fmtTime(1700000000000 - 7200000)).toBe('2時間前');
|
||||
});
|
||||
|
||||
test('24時間以上は日時文字列', () => {
|
||||
const result = fmtTime(1700000000000 - 86400000 * 2);
|
||||
expect(result).toMatch(/\d+月/);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PRIORITY_CYCLE', () => {
|
||||
test('must → better → want → must の循環', () => {
|
||||
expect(PRIORITY_CYCLE.must).toBe('better');
|
||||
expect(PRIORITY_CYCLE.better).toBe('want');
|
||||
expect(PRIORITY_CYCLE.want).toBe('must');
|
||||
});
|
||||
|
||||
test('3つの優先度が全て定義されている', () => {
|
||||
expect(Object.keys(PRIORITY_CYCLE)).toEqual(['must', 'better', 'want']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PRIORITY_COLORS', () => {
|
||||
test('3つの優先度が全て定義されている', () => {
|
||||
expect(Object.keys(PRIORITY_COLORS)).toEqual(['must', 'better', 'want']);
|
||||
});
|
||||
|
||||
test('各色に bg, text, light, border がある', () => {
|
||||
for (const p of ['must', 'better', 'want'] as const) {
|
||||
expect(PRIORITY_COLORS[p]).toHaveProperty('bg');
|
||||
expect(PRIORITY_COLORS[p]).toHaveProperty('text');
|
||||
expect(PRIORITY_COLORS[p]).toHaveProperty('light');
|
||||
expect(PRIORITY_COLORS[p]).toHaveProperty('border');
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('HIGHLIGHT_COLORS', () => {
|
||||
test('3つの優先度が全て定義されている', () => {
|
||||
expect(Object.keys(HIGHLIGHT_COLORS)).toEqual(['must', 'better', 'want']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('authHeaders', () => {
|
||||
test('トークンありの場合 Authorization を含む', () => {
|
||||
const h = authHeaders('my-token');
|
||||
expect(h['Authorization']).toBe('Bearer my-token');
|
||||
expect(h['Content-Type']).toBe('application/json');
|
||||
});
|
||||
|
||||
test('トークンなしの場合 Authorization を含まない', () => {
|
||||
const h = authHeaders('');
|
||||
expect(h['Authorization']).toBeUndefined();
|
||||
expect(h['Content-Type']).toBe('application/json');
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapNormToOrig', () => {
|
||||
test('空白なしの単純マッチ', () => {
|
||||
expect(mapNormToOrig('hello world', 0, 5)).toEqual([0, 5]);
|
||||
});
|
||||
|
||||
test('余分な空白がある場合の正規化マッチ', () => {
|
||||
// 正規化後のインデックス 6-11 は orig で 6-12 に対応(余分な空白を含む)
|
||||
expect(mapNormToOrig('hello world', 6, 11)).toEqual([6, 12]);
|
||||
});
|
||||
|
||||
test('範囲外の normStart は null を返す', () => {
|
||||
expect(mapNormToOrig('short', 100, 105)).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,168 @@
|
||||
import { describe, test, expect, beforeEach, vi } from 'vitest';
|
||||
import { readFileSync } from 'fs';
|
||||
import { resolve } from 'path';
|
||||
|
||||
const WIDGET_JS = readFileSync(
|
||||
resolve(__dirname, '../public/widget.js'),
|
||||
'utf-8'
|
||||
);
|
||||
|
||||
function loadWidget() {
|
||||
document.head.innerHTML = '';
|
||||
document.body.innerHTML = '<p>テストテキスト</p>';
|
||||
|
||||
const script = document.createElement('script');
|
||||
script.src = 'https://my-app.vercel.app/widget.js';
|
||||
script.dataset.token = 'test-token-abc';
|
||||
Object.defineProperty(document, 'currentScript', {
|
||||
value: script,
|
||||
writable: true,
|
||||
configurable: true,
|
||||
});
|
||||
|
||||
window.localStorage.clear();
|
||||
|
||||
vi.stubGlobal(
|
||||
'fetch',
|
||||
vi.fn(() =>
|
||||
Promise.resolve({
|
||||
json: () => Promise.resolve([]),
|
||||
ok: true,
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
// eslint-disable-next-line no-eval
|
||||
eval(WIDGET_JS);
|
||||
}
|
||||
|
||||
describe('widget.js 初期化', () => {
|
||||
beforeEach(() => {
|
||||
loadWidget();
|
||||
});
|
||||
|
||||
test('トグルボタンが DOM に追加される', () => {
|
||||
const toggle = document.getElementById('fb-toggle');
|
||||
expect(toggle).toBeTruthy();
|
||||
expect(toggle!.tagName).toBe('BUTTON');
|
||||
});
|
||||
|
||||
test('サイドバーが DOM に追加される', () => {
|
||||
const sidebar = document.getElementById('fb-sidebar');
|
||||
expect(sidebar).toBeTruthy();
|
||||
});
|
||||
|
||||
test('スタイルが注入される', () => {
|
||||
const style = document.getElementById('fb-widget-styles');
|
||||
expect(style).toBeTruthy();
|
||||
expect(style!.tagName).toBe('STYLE');
|
||||
});
|
||||
|
||||
test('CSS 変数が定義されている', () => {
|
||||
const style = document.getElementById('fb-widget-styles');
|
||||
const css = style!.textContent || '';
|
||||
expect(css).toContain('--fb-bg');
|
||||
expect(css).toContain('--fb-fg');
|
||||
expect(css).toContain('--fb-accent');
|
||||
expect(css).toContain('--fb-border');
|
||||
expect(css).toContain('--fb-muted');
|
||||
expect(css).toContain('--fb-primary');
|
||||
expect(css).toContain('--fb-destructive');
|
||||
});
|
||||
|
||||
test('トグルボタンに「コメント」ラベルがある', () => {
|
||||
const toggle = document.getElementById('fb-toggle');
|
||||
expect(toggle!.innerHTML).toContain('コメント');
|
||||
});
|
||||
|
||||
test('サイドバーにフィルタボタンがある', () => {
|
||||
const sidebar = document.getElementById('fb-sidebar');
|
||||
const html = sidebar!.innerHTML;
|
||||
expect(html).toContain('未解決');
|
||||
expect(html).toContain('解決済');
|
||||
expect(html).toContain('すべて');
|
||||
});
|
||||
|
||||
test('サイドバーにリサイズハンドルがある', () => {
|
||||
const sidebar = document.getElementById('fb-sidebar');
|
||||
const handle = sidebar!.querySelector('.fb-resize-handle');
|
||||
expect(handle).toBeTruthy();
|
||||
});
|
||||
|
||||
test('サイドバーは初期状態で非表示(右にオフセット)', () => {
|
||||
const sidebar = document.getElementById('fb-sidebar') as HTMLElement;
|
||||
const right = parseInt(sidebar.style.right, 10);
|
||||
expect(right).toBeLessThan(0);
|
||||
});
|
||||
|
||||
test('名前未入力時に名前入力ダイアログが表示される', () => {
|
||||
const overlay = document.getElementById('fb-name-overlay');
|
||||
expect(overlay).toBeTruthy();
|
||||
expect(overlay!.innerHTML).toContain('ようこそ');
|
||||
});
|
||||
});
|
||||
|
||||
describe('widget.js ハイライト CSS', () => {
|
||||
beforeEach(() => {
|
||||
loadWidget();
|
||||
});
|
||||
|
||||
test('ハイライトスタイルが優先度別に3色定義されている', () => {
|
||||
const style = document.getElementById('fb-widget-styles');
|
||||
const css = style!.textContent || '';
|
||||
expect(css).toContain('.fb-highlight-must');
|
||||
expect(css).toContain('.fb-highlight-better');
|
||||
expect(css).toContain('.fb-highlight-want');
|
||||
});
|
||||
|
||||
test('カードスタイルが定義されている', () => {
|
||||
const style = document.getElementById('fb-widget-styles');
|
||||
const css = style!.textContent || '';
|
||||
expect(css).toContain('.fb-card');
|
||||
expect(css).toContain('.fb-card-head');
|
||||
expect(css).toContain('.fb-badge-p');
|
||||
});
|
||||
|
||||
test('ポップアップスタイルが定義されている', () => {
|
||||
const style = document.getElementById('fb-widget-styles');
|
||||
const css = style!.textContent || '';
|
||||
expect(css).toContain('.fb-popup');
|
||||
expect(css).toContain('.fb-popup-pri');
|
||||
expect(css).toContain('.fb-popup-actions');
|
||||
});
|
||||
});
|
||||
|
||||
describe('widget.js コメントなし状態', () => {
|
||||
beforeEach(() => {
|
||||
loadWidget();
|
||||
});
|
||||
|
||||
test('コメントゼロ時にサイドバーに空状態メッセージが表示される', () => {
|
||||
const sidebar = document.getElementById('fb-sidebar');
|
||||
const html = sidebar!.innerHTML;
|
||||
expect(html).toContain('コメントはまだありません');
|
||||
});
|
||||
|
||||
test('コメントゼロ時にバッジが表示されない', () => {
|
||||
const toggle = document.getElementById('fb-toggle');
|
||||
expect(toggle!.querySelector('.fb-badge')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('widget.js SVG アイコン', () => {
|
||||
beforeEach(() => {
|
||||
loadWidget();
|
||||
});
|
||||
|
||||
test('トグルボタンに SVG アイコンが含まれる', () => {
|
||||
const toggle = document.getElementById('fb-toggle');
|
||||
expect(toggle!.innerHTML).toContain('<svg');
|
||||
});
|
||||
|
||||
test('サイドバーの閉じるボタンに SVG アイコンが含まれる', () => {
|
||||
const sidebar = document.getElementById('fb-sidebar');
|
||||
const closeBtn = sidebar!.querySelector('[data-action="close"]');
|
||||
expect(closeBtn).toBeTruthy();
|
||||
expect(closeBtn!.innerHTML).toContain('<svg');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user