Skip to content

Commit f7b1a8a

Browse files
committed
ハンドラー/ユニットテスト
1 parent cfe99f8 commit f7b1a8a

10 files changed

Lines changed: 1586 additions & 43 deletions

File tree

side-document-js/package-lock.json

Lines changed: 1163 additions & 39 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

side-document-js/package.json

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,25 +36,31 @@
3636
"build": "vite build",
3737
"build:lib": "cross-env BUILD_LIB=true vite build",
3838
"preview": "vite preview",
39-
"check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json"
39+
"check": "svelte-check --tsconfig ./tsconfig.app.json && tsc -p tsconfig.node.json",
40+
"test": "vitest run",
41+
"test:watch": "vitest"
4042
},
4143
"devDependencies": {
4244
"@sveltejs/adapter-static": "^3.0.8",
4345
"@sveltejs/vite-plugin-svelte": "^6.0.0",
46+
"@testing-library/jest-dom": "^6.9.1",
4447
"@tsconfig/svelte": "^5.0.4",
4548
"@types/qrcode": "^1.5.5",
49+
"@vitest/browser": "^4.1.1",
4650
"autoprefixer": "^10.4.21",
4751
"cross-env": "^10.0.0",
4852
"cssnano": "^7.1.0",
53+
"happy-dom": "^20.8.8",
4954
"postcss": "^8.5.6",
5055
"postcss-extend-rule": "^4.0.0",
5156
"postcss-load-config": "^6.0.1",
5257
"qrcode": "^1.5.4",
5358
"svelte": "^5.35.5",
5459
"svelte-check": "^4.2.2",
5560
"typescript": "~5.8.3",
61+
"vite": "^7.0.4",
5662
"vite-plugin-dts": "^4.5.4",
57-
"vite": "^7.0.4"
63+
"vitest": "^4.1.1"
5864
},
5965
"browserslist": [
6066
"last 2 version",

side-document-js/src/SideDocument.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,11 @@ export class SideDocument {
143143
/**
144144
* 永続化対象外のプロパティ
145145
*/
146-
ignorePersistProps: []
146+
ignorePersistProps: [],
147+
onOpen: undefined,
148+
onClose: undefined,
149+
onResize: undefined,
150+
onPositionChange: undefined,
147151
});
148152

149153

side-document-js/src/lib/SideDocumentDrawer.svelte

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -376,6 +376,8 @@
376376
frameElement.style.userSelect = "auto";
377377
frameElement.style.opacity = "1";
378378
}
379+
380+
option.onResize?.(drawerWidthPx);
379381
}
380382
381383
/**
@@ -404,6 +406,8 @@
404406
drawerPositionClass === "left" ? "top-left" : "top-right";
405407
}
406408
}
409+
410+
option.onPositionChange?.(drawerPositionClass);
407411
}
408412
409413
function getOpenUrl(): string | null {
@@ -511,13 +515,15 @@
511515
}
512516
513517
isOpened = true;
518+
option.onOpen?.(url ?? frameSrc);
514519
}
515520
516521
/**
517522
* ドロワーを閉じます。
518523
*/
519524
export function close() {
520525
isOpened = false;
526+
option.onClose?.();
521527
}
522528
523529
/**

side-document-js/src/types.ts

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,30 @@
11

2+
/**
3+
* コールバック
4+
*/
5+
export interface SideDocumentCallbacks {
6+
/**
7+
* ドロワーが開いたときに呼ばれる
8+
*/
9+
onOpen?: (src: string | null | undefined) => void;
10+
/**
11+
* ドロワーが閉じたときに呼ばれる
12+
*/
13+
onClose?: () => void;
14+
/**
15+
* ドロワーの幅が変更されたときに呼ばれる(リサイズ完了時)
16+
*/
17+
onResize?: (widthPx: number) => void;
18+
/**
19+
* ドロワーの表示位置が変更されたときに呼ばれる
20+
*/
21+
onPositionChange?: (position: 'left' | 'right') => void;
22+
}
223

324
/**
425
* オプション
526
*/
6-
export interface SideDocumentInternalOption {
27+
export interface SideDocumentInternalOption extends SideDocumentCallbacks {
728
/**
829
* コンテナセレクター
930
*/
Lines changed: 269 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,269 @@
1+
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'
2+
import { flushSync, tick } from 'svelte'
3+
import { SideDocument, initialize } from '../../src/SideDocument'
4+
5+
// QRCode は Canvas が必要なためモック
6+
vi.mock('qrcode', () => ({
7+
default: {
8+
toDataURL: vi.fn().mockResolvedValue('data:image/png;base64,mock'),
9+
},
10+
}))
11+
12+
/** Shadow DOM 内の要素を取得するヘルパー */
13+
function getShadowElement<T extends Element>(selector: string): T | null {
14+
const host = document.querySelector('#sd-container')
15+
return (host?.shadowRoot?.querySelector(selector) ?? null) as T | null
16+
}
17+
18+
describe('SideDocument', () => {
19+
let sd: SideDocument
20+
21+
afterEach(() => {
22+
sd?.destroy()
23+
localStorage.clear()
24+
})
25+
26+
// ─────────────────────────────────────────────
27+
// 初期化
28+
// ─────────────────────────────────────────────
29+
describe('initialize()', () => {
30+
it('SideDocument インスタンスを返す', async () => {
31+
sd = await initialize()
32+
expect(sd).toBeInstanceOf(SideDocument)
33+
})
34+
35+
it('#sd-container が document.body に追加される', async () => {
36+
sd = await initialize()
37+
expect(document.querySelector('#sd-container')).not.toBeNull()
38+
})
39+
40+
it('initialize() の関数エクスポートでも初期化できる', async () => {
41+
sd = await initialize({ primaryColor: '#ff0000' })
42+
expect(sd).toBeInstanceOf(SideDocument)
43+
})
44+
})
45+
46+
// ─────────────────────────────────────────────
47+
// destroy()
48+
// ─────────────────────────────────────────────
49+
describe('destroy()', () => {
50+
it('#sd-container が DOM から削除される', async () => {
51+
sd = await initialize()
52+
sd.destroy()
53+
expect(document.querySelector('#sd-container')).toBeNull()
54+
})
55+
56+
it('destroy() を複数回呼んでもエラーにならない', async () => {
57+
sd = await initialize()
58+
expect(() => {
59+
sd.destroy()
60+
sd.destroy()
61+
}).not.toThrow()
62+
})
63+
})
64+
65+
// ─────────────────────────────────────────────
66+
// ドロワー開閉
67+
// ─────────────────────────────────────────────
68+
describe('ドロワーの開閉', () => {
69+
beforeEach(async () => {
70+
sd = await initialize()
71+
})
72+
73+
it('初期状態では閉じている', () => {
74+
expect(sd.isOpenedDrawer()).toBe(false)
75+
})
76+
77+
it('openDrawer() で開く', async () => {
78+
sd.openDrawer()
79+
flushSync()
80+
expect(sd.isOpenedDrawer()).toBe(true)
81+
})
82+
83+
it('closeDrawer() で閉じる', async () => {
84+
sd.openDrawer()
85+
flushSync()
86+
sd.closeDrawer()
87+
flushSync()
88+
expect(sd.isOpenedDrawer()).toBe(false)
89+
})
90+
91+
it('toggleDrawer() で開閉を切り替える', async () => {
92+
sd.toggleDrawer()
93+
flushSync()
94+
expect(sd.isOpenedDrawer()).toBe(true)
95+
96+
sd.toggleDrawer()
97+
flushSync()
98+
expect(sd.isOpenedDrawer()).toBe(false)
99+
})
100+
})
101+
102+
// ─────────────────────────────────────────────
103+
// コールバック
104+
// ─────────────────────────────────────────────
105+
describe('コールバック', () => {
106+
describe('onOpen', () => {
107+
it('openDrawer(url) 呼び出し時に url を渡して呼ばれる', async () => {
108+
const onOpen = vi.fn()
109+
sd = await initialize({ onOpen })
110+
sd.openDrawer('https://example.com')
111+
expect(onOpen).toHaveBeenCalledOnce()
112+
expect(onOpen).toHaveBeenCalledWith('https://example.com')
113+
})
114+
115+
it('openDrawer() を url なしで呼んだ場合は defaultSrc を渡す', async () => {
116+
const onOpen = vi.fn()
117+
sd = await initialize({ defaultSrc: 'https://default.com', onOpen })
118+
sd.openDrawer()
119+
expect(onOpen).toHaveBeenCalledWith('https://default.com')
120+
})
121+
122+
it('openDrawer() を url なしかつ defaultSrc なしの場合は空文字を渡す', async () => {
123+
const onOpen = vi.fn()
124+
sd = await initialize({ onOpen })
125+
sd.openDrawer()
126+
expect(onOpen).toHaveBeenCalledWith('')
127+
})
128+
129+
it('openDrawer() を複数回呼ぶと呼び出し回数が増える', async () => {
130+
const onOpen = vi.fn()
131+
sd = await initialize({ onOpen })
132+
sd.openDrawer('https://a.com')
133+
sd.closeDrawer()
134+
sd.openDrawer('https://b.com')
135+
expect(onOpen).toHaveBeenCalledTimes(2)
136+
expect(onOpen).toHaveBeenLastCalledWith('https://b.com')
137+
})
138+
})
139+
140+
describe('onClose', () => {
141+
it('closeDrawer() 呼び出し時に呼ばれる', async () => {
142+
const onClose = vi.fn()
143+
sd = await initialize({ onClose })
144+
sd.openDrawer()
145+
sd.closeDrawer()
146+
expect(onClose).toHaveBeenCalledOnce()
147+
})
148+
149+
it('ドロワーが閉じている状態で closeDrawer() を呼んでも呼ばれる', async () => {
150+
const onClose = vi.fn()
151+
sd = await initialize({ onClose })
152+
sd.closeDrawer()
153+
expect(onClose).toHaveBeenCalledOnce()
154+
})
155+
})
156+
157+
describe('onOpen / onClose の組み合わせ', () => {
158+
it('open → close → open の順で正しく呼ばれる', async () => {
159+
const onOpen = vi.fn()
160+
const onClose = vi.fn()
161+
sd = await initialize({ onOpen, onClose })
162+
163+
sd.openDrawer('https://a.com')
164+
sd.closeDrawer()
165+
sd.openDrawer('https://b.com')
166+
167+
expect(onOpen).toHaveBeenCalledTimes(2)
168+
expect(onClose).toHaveBeenCalledTimes(1)
169+
})
170+
})
171+
172+
describe('onPositionChange', () => {
173+
it('位置変更ボタンをクリックすると呼ばれる', async () => {
174+
const onPositionChange = vi.fn()
175+
sd = await initialize({
176+
drawerPosition: 'right',
177+
onPositionChange,
178+
})
179+
sd.openDrawer()
180+
await tick() // Svelte が {#if isOpened} ブロックを描画するのを待つ
181+
182+
const btn = getShadowElement<HTMLButtonElement>(
183+
'[aria-label="表示位置を変更"]',
184+
)
185+
expect(btn).not.toBeNull()
186+
btn!.click()
187+
188+
expect(onPositionChange).toHaveBeenCalledOnce()
189+
expect(onPositionChange).toHaveBeenCalledWith(
190+
expect.stringMatching(/^(left|right)$/),
191+
)
192+
})
193+
194+
it('right → left → right と交互に切り替わる', async () => {
195+
const onPositionChange = vi.fn()
196+
sd = await initialize({
197+
drawerPosition: 'right',
198+
onPositionChange,
199+
})
200+
sd.openDrawer()
201+
await tick()
202+
203+
const btn = getShadowElement<HTMLButtonElement>(
204+
'[aria-label="表示位置を変更"]',
205+
)!
206+
btn.click()
207+
expect(onPositionChange).toHaveBeenLastCalledWith('left')
208+
btn.click()
209+
expect(onPositionChange).toHaveBeenLastCalledWith('right')
210+
})
211+
})
212+
213+
describe('onResize', () => {
214+
it('リサイズバーのドラッグ完了時に新しい幅で呼ばれる', async () => {
215+
const onResize = vi.fn()
216+
sd = await initialize({ onResize, drawerWidth: 380 })
217+
sd.openDrawer()
218+
await tick()
219+
220+
const resizeBar = getShadowElement<HTMLSpanElement>('.sd-resize-bar')
221+
expect(resizeBar).not.toBeNull()
222+
223+
// mousedown でリサイズ開始
224+
resizeBar!.dispatchEvent(
225+
new MouseEvent('mousedown', { bubbles: true, clientX: 500 }),
226+
)
227+
// mousemove でドラッグ(左へ 100px)
228+
window.dispatchEvent(
229+
new MouseEvent('mousemove', { bubbles: true, clientX: 400 }),
230+
)
231+
// mouseup でリサイズ完了
232+
window.dispatchEvent(new MouseEvent('mouseup', { bubbles: true }))
233+
234+
expect(onResize).toHaveBeenCalledOnce()
235+
expect(onResize).toHaveBeenCalledWith(expect.any(Number))
236+
})
237+
})
238+
})
239+
240+
// ─────────────────────────────────────────────
241+
// persistState / clearPersistedState
242+
// ─────────────────────────────────────────────
243+
describe('clearPersistedState()', () => {
244+
it('localStorage からドロワーの状態を削除する', async () => {
245+
sd = await initialize({ persistState: true, storageKeyPrefix: 'test' })
246+
sd.openDrawer()
247+
await tick()
248+
249+
// localStorage に何か保存されていることを確認
250+
const key = 'test-state'
251+
// 少し待って save のスロットリングが走るようにする
252+
await new Promise(resolve => setTimeout(resolve, 600))
253+
expect(localStorage.getItem(key)).not.toBeNull()
254+
255+
sd.clearPersistedState()
256+
expect(localStorage.getItem(key)).toBeNull()
257+
})
258+
})
259+
260+
// ─────────────────────────────────────────────
261+
// update()
262+
// ─────────────────────────────────────────────
263+
describe('update()', () => {
264+
it('オプションを動的に更新できる', async () => {
265+
sd = await initialize({ primaryColor: '#000000' })
266+
await expect(sd.update({ primaryColor: '#ff0000' })).resolves.not.toThrow()
267+
})
268+
})
269+
})

0 commit comments

Comments
 (0)