Skip to content

Commit 211fe1e

Browse files
committed
✅ add testcase and fix typo #294
1 parent 8deaf56 commit 211fe1e

File tree

8 files changed

+406
-55
lines changed

8 files changed

+406
-55
lines changed
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
describe('ClassThrown helpers', () => {
4+
it('creates NavigateThrown with route payload', () => {
5+
const route = { name: 'home' };
6+
const thrown = new NavigateThrown(route);
7+
expect(thrown.name).toBe('NavigateThrown');
8+
expect(thrown.route).toBe(route);
9+
});
10+
11+
it('creates IgnoredThrown with custom message', () => {
12+
const thrown = new IgnoredThrown('skip this');
13+
expect(thrown.name).toBe('IgnoredThrown');
14+
expect(thrown.message).toBe('skip this');
15+
});
16+
17+
it('creates DataThrown with arbitrary payload', () => {
18+
const payload = { foo: 'bar' };
19+
const thrown = new DataThrown('custom', payload);
20+
expect(thrown.name).toBe('DataThrown');
21+
expect(thrown.type).toBe('custom');
22+
expect(thrown.data).toBe(payload);
23+
});
24+
25+
it('wraps notify data in NotifyThrown', () => {
26+
const notifyFromString = new NotifyThrown({ message: 'warning', notifyLevel: GlobalNotifyLevel.Warning });
27+
expect(notifyFromString.notify).toMatchObject({ message: 'warning', notifyLevel: GlobalNotifyLevel.Warning });
28+
29+
const notifyFromObject = new NotifyThrown({
30+
message: 'ok',
31+
notifyLevel: GlobalNotifyLevel.Success,
32+
notifyStyle: GlobalNotifyStyle.Toast,
33+
});
34+
expect(notifyFromObject.notify).toMatchObject({
35+
message: 'ok',
36+
notifyLevel: GlobalNotifyLevel.Success,
37+
notifyStyle: GlobalNotifyStyle.Toast,
38+
});
39+
});
40+
41+
it('creates NoticeThrown from arrays and varargs', () => {
42+
const noticeA = { message: 'A' } as I18nNotice;
43+
const noticeB = { message: 'B' } as I18nNotice;
44+
45+
const fromArray = new NoticeThrown([noticeA, noticeB]);
46+
expect(fromArray.notices).toEqual([noticeA, noticeB]);
47+
48+
const fromArgs = new NoticeThrown(noticeA, noticeB);
49+
expect(fromArgs.notices).toEqual([noticeA, noticeB]);
50+
});
51+
});
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { defineComponent } from 'vue';
3+
import { createI18n } from 'vue-i18n';
4+
import { mount } from '@vue/test-utils';
5+
6+
describe('useLocalizeMessage', () => {
7+
it('localizes message using vue-i18n context', () => {
8+
const i18n = createI18n({
9+
legacy: false,
10+
locale: 'en-US',
11+
messages: {
12+
'en-US': {
13+
greet: 'Hello {0}',
14+
world: 'World',
15+
},
16+
},
17+
});
18+
19+
const TestComponent = defineComponent({
20+
setup(_props, { expose }) {
21+
const localize = useLocalizeMessage();
22+
expose({ localize });
23+
return () => null;
24+
},
25+
});
26+
27+
const wrapper = mount(TestComponent, {
28+
global: {
29+
plugins: [i18n],
30+
},
31+
});
32+
33+
const localize = (wrapper.vm as unknown as { localize: ReturnType<typeof useLocalizeMessage> }).localize;
34+
const result = localize({ i18nCode: 'greet', i18nArgs: ['world'], message: 'fallback' }, true);
35+
36+
expect(result).toBe('Hello World');
37+
});
38+
39+
it('returns fallback when translation code missing', () => {
40+
const i18n = createI18n({ legacy: false, locale: 'en-US', messages: { 'en-US': {} } });
41+
42+
const TestComponent = defineComponent({
43+
setup(_props, { expose }) {
44+
const localize = useLocalizeMessage();
45+
expose({ localize });
46+
return () => null;
47+
},
48+
});
49+
50+
const wrapper = mount(TestComponent, {
51+
global: {
52+
plugins: [i18n],
53+
},
54+
});
55+
56+
const localize = (wrapper.vm as unknown as { localize: ReturnType<typeof useLocalizeMessage> }).localize;
57+
expect(localize(undefined, 'default')).toBe('default');
58+
});
59+
});
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
import { describe, expect, it } from 'vitest';
2+
import { defineComponent, h } from 'vue';
3+
import { mount } from '@vue/test-utils';
4+
5+
const UniqueComponent = defineComponent({
6+
setup(_props, { expose }) {
7+
const unique = useUniqueKey();
8+
expose({ unique });
9+
return () => h('div');
10+
},
11+
});
12+
13+
describe('useUniqueKey', () => {
14+
it('generates deterministic keys based on component uid and md5 hash', () => {
15+
const wrapper = mount(UniqueComponent);
16+
const unique = (wrapper.vm as unknown as { unique: (...args: unknown[]) => string }).unique;
17+
18+
const key = unique('foo', 123, { nested: true });
19+
const uid = (wrapper.vm as SafeAny).$?.uid ?? wrapper.vm.$.uid;
20+
expect(key.startsWith(`${uid}-`)).toBe(true);
21+
22+
// same args produce identical hash, different args produce different hash
23+
expect(unique('foo', 123, { nested: true })).toBe(key);
24+
expect(unique('foo', 456)).not.toBe(key);
25+
});
26+
27+
it('throws when used outside of component setup', () => {
28+
expect(() => useUniqueKey()).toThrowError('useUniqueKey must be used within a Vue component setup');
29+
});
30+
});
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
2+
3+
describe('common-blob utilities', () => {
4+
beforeEach(() => {
5+
vi.useFakeTimers();
6+
});
7+
8+
afterEach(() => {
9+
vi.useRealTimers();
10+
vi.restoreAllMocks();
11+
});
12+
13+
it('delegates to navigator.msSaveOrOpenBlob when available', () => {
14+
const blob = new Blob(['hello']);
15+
const navigatorStub = window.navigator as Navigator & { msSaveOrOpenBlob?: (blob: Blob, name?: string) => void };
16+
navigatorStub.msSaveOrOpenBlob = vi.fn();
17+
18+
saveBlobFile({ name: 'hello.txt', blob });
19+
20+
expect(navigatorStub.msSaveOrOpenBlob).toHaveBeenCalledWith(blob, 'hello.txt');
21+
delete navigatorStub.msSaveOrOpenBlob;
22+
});
23+
24+
it('creates anchor element and revokes url for Blob', () => {
25+
const blob = new Blob(['world']);
26+
const appendSpy = vi.spyOn(document.body, 'appendChild');
27+
const removeSpy = vi.spyOn(document.body, 'removeChild');
28+
vi.spyOn(document, 'createElement');
29+
const dispatchSpy = vi.spyOn(HTMLAnchorElement.prototype, 'dispatchEvent');
30+
const objectUrlSpy = vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:mock');
31+
const revokeSpy = vi.spyOn(URL, 'revokeObjectURL');
32+
33+
saveBlobFile(blob, 'fallback.txt');
34+
35+
expect(objectUrlSpy).toHaveBeenCalledWith(blob);
36+
expect(appendSpy).toHaveBeenCalled();
37+
expect(dispatchSpy).toHaveBeenCalled();
38+
39+
vi.runAllTimers();
40+
41+
expect(removeSpy).toHaveBeenCalled();
42+
expect(revokeSpy).toHaveBeenCalledWith('blob:mock');
43+
});
44+
45+
it('supports legacy MouseEvent fallback when constructor throws', () => {
46+
const originalMouseEvent = globalThis.MouseEvent;
47+
const mouseEventSpy = vi.fn(() => {
48+
throw new Error('unsupported');
49+
});
50+
globalThis.MouseEvent = mouseEventSpy as unknown as typeof MouseEvent;
51+
52+
const originalCreateEvent = document.createEvent.bind(document);
53+
const createEventSpy = vi.spyOn(document, 'createEvent').mockImplementation((type: string) => {
54+
const event = originalCreateEvent(type) as MouseEvent & { initMouseEvent?: (...args: unknown[]) => void };
55+
if (type === 'MouseEvent' && typeof event.initMouseEvent !== 'function') {
56+
event.initMouseEvent = vi.fn();
57+
}
58+
return event;
59+
});
60+
61+
const objectUrlSpy = vi.spyOn(URL, 'createObjectURL').mockReturnValue('blob:legacy');
62+
const revokeSpy = vi.spyOn(URL, 'revokeObjectURL');
63+
64+
saveBlobFile(new Blob(['legacy']), 'legacy.txt');
65+
66+
expect(mouseEventSpy).toHaveBeenCalled();
67+
expect(createEventSpy).toHaveBeenCalledWith('MouseEvent');
68+
69+
vi.runAllTimers();
70+
71+
expect(objectUrlSpy).toHaveBeenCalled();
72+
expect(revokeSpy).toHaveBeenCalledWith('blob:legacy');
73+
74+
createEventSpy.mockRestore();
75+
globalThis.MouseEvent = originalMouseEvent;
76+
});
77+
78+
it('parses filenames from Content-Disposition header', () => {
79+
expect(parseContentDispositionFilename('attachment; filename="report.txt"')).toBe('report.txt');
80+
expect(parseContentDispositionFilename('attachment; filename="fallback.txt"; filename*=UTF-8\'\'%E4%B8%AD%E6%96%87.txt')).toBe('中文.txt');
81+
expect(parseContentDispositionFilename('attachment')).toBeUndefined();
82+
});
83+
84+
it('detects blob-like objects', () => {
85+
const fakeBlob = { arrayBuffer: () => Promise.resolve(new ArrayBuffer(0)), text: () => Promise.resolve('') };
86+
expect(isBlobLike(new Blob(['data']))).toBe(true);
87+
expect(isBlobLike(fakeBlob)).toBe(true);
88+
expect(isBlobLike({})).toBe(false);
89+
});
90+
});
Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,123 @@
1+
import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest';
2+
3+
describe('common notify helpers', () => {
4+
beforeEach(() => {
5+
vi.useFakeTimers();
6+
});
7+
8+
afterEach(() => {
9+
vi.useRealTimers();
10+
});
11+
12+
it('handles unlimited stacked notifications', () => {
13+
const calls: Array<{ index: number; data: string }> = [];
14+
const notify = createStackedNotify<string>((index, data, close) => {
15+
calls.push({ index, data });
16+
close();
17+
});
18+
19+
notify('first');
20+
vi.runOnlyPendingTimers();
21+
notify('second');
22+
vi.runOnlyPendingTimers();
23+
24+
expect(calls).toEqual([
25+
{ index: 0, data: 'first' },
26+
{ index: 0, data: 'second' },
27+
]);
28+
});
29+
30+
it('queues stacked notifications when all slots are busy', () => {
31+
const calls: string[] = [];
32+
const closers: Array<() => void> = [];
33+
const notify = createStackedNotify<string>((index, data, close) => {
34+
calls.push(`${index}:${data}`);
35+
closers.push(() => close());
36+
}, 2);
37+
38+
for (let i = 0; i < 5; i++) {
39+
notify(`job-${i}`);
40+
}
41+
42+
notify('queued');
43+
expect(calls).toHaveLength(5);
44+
45+
closers[0]!();
46+
vi.runOnlyPendingTimers();
47+
48+
expect(calls).toHaveLength(6);
49+
expect(calls.at(-1)).toMatch(/\d:queued/);
50+
});
51+
52+
it('processes notifications sequentially', () => {
53+
const calls: string[] = [];
54+
const notify = createSingledNotify<string>((data, close) => {
55+
calls.push(data);
56+
close();
57+
});
58+
59+
notify('a');
60+
notify('b');
61+
62+
expect(calls).toEqual(['a']);
63+
vi.runOnlyPendingTimers();
64+
expect(calls).toEqual(['a', 'b']);
65+
});
66+
67+
it('toggles refs and callbacks correctly', () => {
68+
const modal = ref(false);
69+
const callback = vi.fn<(open: boolean, payload?: string) => void>();
70+
const optional = vi.fn<(open: boolean, payload?: number) => void>();
71+
72+
const toggled = createToggledNotify<{
73+
modal: typeof modal;
74+
callback: (open: boolean, payload?: string) => void;
75+
optional: (open: boolean, payload?: number) => void;
76+
}>();
77+
78+
toggled.init('modal', modal);
79+
toggled.init('callback', callback);
80+
toggled.init('optional', optional, true);
81+
82+
toggled.open('modal');
83+
expect(modal.value).toBe(true);
84+
85+
toggled.toggle('modal');
86+
expect(modal.value).toBe(false);
87+
88+
toggled.openExclusive('callback', 'payload');
89+
expect(callback).toHaveBeenLastCalledWith(true, 'payload');
90+
91+
toggled.open('optional', 42);
92+
expect(optional).toHaveBeenLastCalledWith(true, 42);
93+
94+
toggled.closeAll();
95+
expect(callback).toHaveBeenLastCalledWith(false, undefined);
96+
expect(optional).toHaveBeenLastCalledWith(false, undefined);
97+
});
98+
99+
it('creates app notify with default warning level', () => {
100+
const { eventBus, newThrown } = createAppNotify('toast', GlobalNotifyStyle.Toast);
101+
102+
const notifyFromString = newThrown('be careful');
103+
expect(notifyFromString).toBeInstanceOf(NotifyThrown);
104+
expect(notifyFromString.notify).toMatchObject({
105+
message: 'be careful',
106+
notifyLevel: GlobalNotifyLevel.Warning,
107+
notifyStyle: GlobalNotifyStyle.Toast,
108+
});
109+
110+
const notifyFromObject = newThrown({
111+
message: 'ok',
112+
notifyLevel: GlobalNotifyLevel.Success,
113+
});
114+
expect(notifyFromObject.notify).toMatchObject({
115+
message: 'ok',
116+
notifyLevel: GlobalNotifyLevel.Success,
117+
notifyStyle: GlobalNotifyStyle.Toast,
118+
});
119+
120+
expect(typeof eventBus.emit).toBe('function');
121+
expect(typeof eventBus.on).toBe('function');
122+
});
123+
});

0 commit comments

Comments
 (0)