haori 0.4.3 → 0.4.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.ja.md +4 -1
- package/README.md +4 -1
- package/dist/haori.cjs.js +12 -10
- package/dist/haori.cjs.js.map +1 -1
- package/dist/haori.es.js +997 -856
- package/dist/haori.es.js.map +1 -1
- package/dist/haori.iife.js +13 -11
- package/dist/haori.iife.js.map +1 -1
- package/dist/index.d.ts +66 -3
- package/dist/package.json +1 -1
- package/dist/src/core.d.ts +17 -0
- package/dist/src/core.d.ts.map +1 -1
- package/dist/src/core.js +75 -22
- package/dist/src/core.js.map +1 -1
- package/dist/src/event_dispatcher.d.ts +10 -2
- package/dist/src/event_dispatcher.d.ts.map +1 -1
- package/dist/src/event_dispatcher.js +21 -3
- package/dist/src/event_dispatcher.js.map +1 -1
- package/dist/src/form.d.ts +12 -1
- package/dist/src/form.d.ts.map +1 -1
- package/dist/src/form.js +20 -3
- package/dist/src/form.js.map +1 -1
- package/dist/src/fragment.d.ts +27 -0
- package/dist/src/fragment.d.ts.map +1 -1
- package/dist/src/fragment.js +62 -8
- package/dist/src/fragment.js.map +1 -1
- package/dist/src/haori.d.ts +10 -2
- package/dist/src/haori.d.ts.map +1 -1
- package/dist/src/haori.js +23 -11
- package/dist/src/haori.js.map +1 -1
- package/dist/src/observer.d.ts.map +1 -1
- package/dist/src/observer.js +4 -0
- package/dist/src/observer.js.map +1 -1
- package/dist/src/procedure.d.ts +18 -3
- package/dist/src/procedure.d.ts.map +1 -1
- package/dist/src/procedure.js +78 -27
- package/dist/src/procedure.js.map +1 -1
- package/dist/tests/core.test.js +40 -0
- package/dist/tests/core.test.js.map +1 -1
- package/dist/tests/fetch-and-procedure-scenarios.test.js +54 -0
- package/dist/tests/fetch-and-procedure-scenarios.test.js.map +1 -1
- package/dist/tests/haori.test.d.ts +2 -0
- package/dist/tests/haori.test.d.ts.map +1 -0
- package/dist/tests/haori.test.js +149 -0
- package/dist/tests/haori.test.js.map +1 -0
- package/dist/tests/helpers/async.d.ts.map +1 -1
- package/dist/tests/helpers/async.js +1 -0
- package/dist/tests/helpers/async.js.map +1 -1
- package/dist/tests/procedure-action-operations.test.js +273 -4
- package/dist/tests/procedure-action-operations.test.js.map +1 -1
- package/dist/tests/procedure.test.js +3 -2
- package/dist/tests/procedure.test.js.map +1 -1
- package/dist/tests/row-operations.test.js +21 -1
- package/dist/tests/row-operations.test.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import Haori from '../src/haori';
|
|
3
|
+
describe('Haori.dialog', () => {
|
|
4
|
+
beforeEach(() => {
|
|
5
|
+
vi.spyOn(window, 'alert').mockImplementation(() => { });
|
|
6
|
+
});
|
|
7
|
+
it('メッセージをそのまま alert に渡す', async () => {
|
|
8
|
+
await Haori.dialog('こんにちは');
|
|
9
|
+
expect(window.alert).toHaveBeenCalledWith('こんにちは');
|
|
10
|
+
});
|
|
11
|
+
it('実改行を含む文字列をそのまま渡す(変質させない)', async () => {
|
|
12
|
+
await Haori.dialog('Hello\nWorld');
|
|
13
|
+
expect(window.alert).toHaveBeenCalledWith('Hello\nWorld');
|
|
14
|
+
});
|
|
15
|
+
it('リテラル \\n を正規化しない(Procedure 経路でのみ正規化)', async () => {
|
|
16
|
+
await Haori.dialog('Hello\\nWorld');
|
|
17
|
+
expect(window.alert).toHaveBeenCalledWith('Hello\\nWorld');
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
describe('Haori.confirm', () => {
|
|
21
|
+
beforeEach(() => {
|
|
22
|
+
vi.spyOn(window, 'confirm').mockReturnValue(true);
|
|
23
|
+
});
|
|
24
|
+
it('メッセージをそのまま confirm に渡す', async () => {
|
|
25
|
+
await Haori.confirm('続けますか?');
|
|
26
|
+
expect(window.confirm).toHaveBeenCalledWith('続けますか?');
|
|
27
|
+
});
|
|
28
|
+
it('実改行を含む文字列をそのまま渡す(変質させない)', async () => {
|
|
29
|
+
await Haori.confirm('続けますか?\nこの操作は取り消せません。');
|
|
30
|
+
expect(window.confirm).toHaveBeenCalledWith('続けますか?\nこの操作は取り消せません。');
|
|
31
|
+
});
|
|
32
|
+
it('リテラル \\n を正規化しない(Procedure 経路でのみ正規化)', async () => {
|
|
33
|
+
await Haori.confirm('続けますか?\\nこの操作は取り消せません。');
|
|
34
|
+
expect(window.confirm).toHaveBeenCalledWith('続けますか?\\nこの操作は取り消せません。');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
describe('Haori.toast', () => {
|
|
38
|
+
beforeEach(() => {
|
|
39
|
+
document.body.innerHTML = '';
|
|
40
|
+
vi.useFakeTimers();
|
|
41
|
+
HTMLElement.prototype.showPopover = vi.fn();
|
|
42
|
+
HTMLElement.prototype.hidePopover = vi.fn();
|
|
43
|
+
});
|
|
44
|
+
afterEach(() => {
|
|
45
|
+
vi.useRealTimers();
|
|
46
|
+
});
|
|
47
|
+
it('level を省略すると info として扱う', async () => {
|
|
48
|
+
Haori.toast('msg');
|
|
49
|
+
const toast = document.querySelector('.haori-toast-info');
|
|
50
|
+
expect(toast).not.toBeNull();
|
|
51
|
+
expect(toast?.getAttribute('aria-live')).toBe('polite');
|
|
52
|
+
});
|
|
53
|
+
it.each(['info', 'warning', 'success'])('level "%s" は aria-live="polite" を設定する', (level) => {
|
|
54
|
+
Haori.toast('msg', level);
|
|
55
|
+
const toast = document.querySelector(`.haori-toast-${level}`);
|
|
56
|
+
expect(toast?.getAttribute('aria-live')).toBe('polite');
|
|
57
|
+
});
|
|
58
|
+
it('level "error" は aria-live="assertive" を設定する', () => {
|
|
59
|
+
Haori.toast('msg', 'error');
|
|
60
|
+
const toast = document.querySelector('.haori-toast-error');
|
|
61
|
+
expect(toast?.getAttribute('aria-live')).toBe('assertive');
|
|
62
|
+
});
|
|
63
|
+
it('3秒後にトーストを非表示にして DOM から削除する', () => {
|
|
64
|
+
const hidePopoverSpy = HTMLElement.prototype.hidePopover;
|
|
65
|
+
Haori.toast('hello', 'info');
|
|
66
|
+
expect(document.querySelector('.haori-toast')).not.toBeNull();
|
|
67
|
+
vi.advanceTimersByTime(3000);
|
|
68
|
+
expect(hidePopoverSpy).toHaveBeenCalledTimes(1);
|
|
69
|
+
expect(document.querySelector('.haori-toast')).toBeNull();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
describe('Haori.addMessage', () => {
|
|
73
|
+
beforeEach(() => {
|
|
74
|
+
document.body.innerHTML = '';
|
|
75
|
+
});
|
|
76
|
+
it('入力要素の場合は親要素に data-message を付与する', async () => {
|
|
77
|
+
const parent = document.createElement('div');
|
|
78
|
+
const input = document.createElement('input');
|
|
79
|
+
parent.appendChild(input);
|
|
80
|
+
document.body.appendChild(parent);
|
|
81
|
+
await Haori.addMessage(input, 'エラー');
|
|
82
|
+
expect(parent.getAttribute('data-message')).toBe('エラー');
|
|
83
|
+
expect(parent.hasAttribute('data-message-level')).toBe(false);
|
|
84
|
+
});
|
|
85
|
+
it('level を指定すると data-message-level が付与される', async () => {
|
|
86
|
+
const parent = document.createElement('div');
|
|
87
|
+
const input = document.createElement('input');
|
|
88
|
+
parent.appendChild(input);
|
|
89
|
+
document.body.appendChild(parent);
|
|
90
|
+
await Haori.addMessage(input, '確認', 'info');
|
|
91
|
+
expect(parent.getAttribute('data-message')).toBe('確認');
|
|
92
|
+
expect(parent.getAttribute('data-message-level')).toBe('info');
|
|
93
|
+
});
|
|
94
|
+
it.each(['info', 'warning', 'error', 'success'])('level "%s" を data-message-level に設定できる', async (level) => {
|
|
95
|
+
const parent = document.createElement('div');
|
|
96
|
+
const input = document.createElement('input');
|
|
97
|
+
parent.appendChild(input);
|
|
98
|
+
document.body.appendChild(parent);
|
|
99
|
+
await Haori.addMessage(input, 'msg', level);
|
|
100
|
+
expect(parent.getAttribute('data-message-level')).toBe(level);
|
|
101
|
+
});
|
|
102
|
+
it('フォーム要素の場合はフォーム自身に付与する', async () => {
|
|
103
|
+
const form = document.createElement('form');
|
|
104
|
+
document.body.appendChild(form);
|
|
105
|
+
await Haori.addMessage(form, 'フォームエラー', 'error');
|
|
106
|
+
expect(form.getAttribute('data-message')).toBe('フォームエラー');
|
|
107
|
+
expect(form.getAttribute('data-message-level')).toBe('error');
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
describe('Haori.clearMessages', () => {
|
|
111
|
+
beforeEach(() => {
|
|
112
|
+
document.body.innerHTML = '';
|
|
113
|
+
});
|
|
114
|
+
it('data-message と data-message-level を両方除去する', async () => {
|
|
115
|
+
const parent = document.createElement('div');
|
|
116
|
+
const input = document.createElement('input');
|
|
117
|
+
parent.appendChild(input);
|
|
118
|
+
document.body.appendChild(parent);
|
|
119
|
+
await Haori.addMessage(input, 'エラー', 'error');
|
|
120
|
+
expect(parent.hasAttribute('data-message-level')).toBe(true);
|
|
121
|
+
await Haori.clearMessages(parent);
|
|
122
|
+
expect(parent.hasAttribute('data-message')).toBe(false);
|
|
123
|
+
expect(parent.hasAttribute('data-message-level')).toBe(false);
|
|
124
|
+
});
|
|
125
|
+
it('子要素の data-message-level も除去する', async () => {
|
|
126
|
+
const form = document.createElement('form');
|
|
127
|
+
document.body.appendChild(form);
|
|
128
|
+
await Haori.addMessage(form, 'エラー', 'warning');
|
|
129
|
+
expect(form.getAttribute('data-message-level')).toBe('warning');
|
|
130
|
+
await Haori.clearMessages(form);
|
|
131
|
+
expect(form.hasAttribute('data-message')).toBe(false);
|
|
132
|
+
expect(form.hasAttribute('data-message-level')).toBe(false);
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
describe('Haori.addErrorMessage', () => {
|
|
136
|
+
beforeEach(() => {
|
|
137
|
+
document.body.innerHTML = '';
|
|
138
|
+
});
|
|
139
|
+
it('addMessage("error") に委譲する', async () => {
|
|
140
|
+
const parent = document.createElement('div');
|
|
141
|
+
const input = document.createElement('input');
|
|
142
|
+
parent.appendChild(input);
|
|
143
|
+
document.body.appendChild(parent);
|
|
144
|
+
await Haori.addErrorMessage(input, 'エラー');
|
|
145
|
+
expect(parent.getAttribute('data-message')).toBe('エラー');
|
|
146
|
+
expect(parent.getAttribute('data-message-level')).toBe('error');
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
//# sourceMappingURL=haori.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"haori.test.js","sourceRoot":"","sources":["../../tests/haori.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"async.d.ts","sourceRoot":"","sources":["../../../tests/helpers/async.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,SAAI,GAAG,OAAO,CAAC,IAAI,CAAC,
|
|
1
|
+
{"version":3,"file":"async.d.ts","sourceRoot":"","sources":["../../../tests/helpers/async.ts"],"names":[],"mappings":"AAEA;;;;;GAKG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,SAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAMjE;AAED;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACpC,SAAS,EAAE,MAAM,OAAO,EACxB,OAAO,GAAE;IAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAM,GACzD,OAAO,CAAC,IAAI,CAAC,CASf"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"async.js","sourceRoot":"","sources":["../../../tests/helpers/async.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,iBAAiB,CAAC;AAEpC;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,MAAM,GAAG,CAAC;IAChD,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,MAAM,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QAC/C,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;QACxB,MAAM,KAAK,CAAC,IAAI,EAAE,CAAC;IACrB,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,SAAwB,EACxB,UAAwD,EAAE;IAE1D,MAAM,EAAC,WAAW,GAAG,WAAW,EAAE,WAAW,GAAG,EAAE,EAAC,GAAG,OAAO,CAAC;IAC9D,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,WAAW,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC;QAC1D,IAAI,SAAS,EAAE,EAAE,CAAC;YAChB,OAAO;QACT,CAAC;QACD,MAAM,iBAAiB,EAAE,CAAC;IAC5B,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,yBAAyB,WAAW,GAAG,CAAC,CAAC;AAC3D,CAAC"}
|
|
1
|
+
{"version":3,"file":"async.js","sourceRoot":"","sources":["../../../tests/helpers/async.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,iBAAiB,CAAC;AAEpC;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,MAAM,GAAG,CAAC;IAChD,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,MAAM,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QAC/C,MAAM,OAAO,CAAC,OAAO,EAAE,CAAC;QACxB,MAAM,IAAI,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;QACrD,MAAM,KAAK,CAAC,IAAI,EAAE,CAAC;IACrB,CAAC;AACH,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,gBAAgB,CACpC,SAAwB,EACxB,UAAwD,EAAE;IAE1D,MAAM,EAAC,WAAW,GAAG,WAAW,EAAE,WAAW,GAAG,EAAE,EAAC,GAAG,OAAO,CAAC;IAC9D,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,WAAW,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC;QAC1D,IAAI,SAAS,EAAE,EAAE,CAAC;YAChB,OAAO;QACT,CAAC;QACD,MAAM,iBAAiB,EAAE,CAAC;IAC5B,CAAC;IACD,MAAM,IAAI,KAAK,CAAC,yBAAyB,WAAW,GAAG,CAAC,CAAC;AAC3D,CAAC"}
|
|
@@ -359,6 +359,7 @@ describe('Procedure action operations', () => {
|
|
|
359
359
|
emailInput.name = 'email';
|
|
360
360
|
form.appendChild(emailInput);
|
|
361
361
|
const btn = document.createElement('button');
|
|
362
|
+
btn.type = 'button';
|
|
362
363
|
btn.setAttribute('data-click-form', '');
|
|
363
364
|
btn.setAttribute('data-click-fetch', 'http://api.test/json-error');
|
|
364
365
|
form.appendChild(btn);
|
|
@@ -378,6 +379,261 @@ describe('Procedure action operations', () => {
|
|
|
378
379
|
form.remove();
|
|
379
380
|
});
|
|
380
381
|
// -----------------------------------------------------------------------
|
|
382
|
+
// data-click-scroll-error
|
|
383
|
+
// -----------------------------------------------------------------------
|
|
384
|
+
describe('data-click-scroll-error', () => {
|
|
385
|
+
let scrollIntoViewSpy;
|
|
386
|
+
beforeEach(() => {
|
|
387
|
+
// jsdom は scrollIntoView を実装していないため先に定義してからスパイする
|
|
388
|
+
Element.prototype.scrollIntoView = () => { };
|
|
389
|
+
scrollIntoViewSpy = vi
|
|
390
|
+
.spyOn(Element.prototype, 'scrollIntoView')
|
|
391
|
+
.mockImplementation(() => { });
|
|
392
|
+
});
|
|
393
|
+
afterEach(() => {
|
|
394
|
+
// vi.restoreAllMocks() は直接代入した () => {} へ戻すだけなので、
|
|
395
|
+
// afterEach でプロパティ自体を削除して jsdom の元の状態(未定義)に戻す
|
|
396
|
+
delete Element.prototype
|
|
397
|
+
.scrollIntoView;
|
|
398
|
+
});
|
|
399
|
+
it('JSON フィールドエラー: エラー要素の scrollIntoView が呼ばれる', async () => {
|
|
400
|
+
vi.spyOn(globalThis, 'fetch').mockImplementation(() => Promise.resolve(new Response(JSON.stringify({ errors: { email: 'メールアドレスが不正です' } }), { status: 422, headers: { 'Content-Type': 'application/json' } })));
|
|
401
|
+
const form = document.createElement('form');
|
|
402
|
+
const emailWrapper = document.createElement('div');
|
|
403
|
+
const emailInput = document.createElement('input');
|
|
404
|
+
emailInput.name = 'email';
|
|
405
|
+
emailWrapper.appendChild(emailInput);
|
|
406
|
+
const btn = document.createElement('button');
|
|
407
|
+
btn.type = 'button';
|
|
408
|
+
btn.setAttribute('data-click-fetch', 'http://api.test/json-field-error');
|
|
409
|
+
btn.setAttribute('data-click-scroll-error', '');
|
|
410
|
+
form.append(emailWrapper, btn);
|
|
411
|
+
document.body.appendChild(form);
|
|
412
|
+
await waitForDomSettled();
|
|
413
|
+
btn.click();
|
|
414
|
+
await waitForCondition(() => emailWrapper.getAttribute('data-message-level') === 'error', { description: 'field error message set' });
|
|
415
|
+
expect(scrollIntoViewSpy).toHaveBeenCalledWith({
|
|
416
|
+
behavior: 'smooth',
|
|
417
|
+
block: 'nearest',
|
|
418
|
+
});
|
|
419
|
+
form.remove();
|
|
420
|
+
});
|
|
421
|
+
it('JSON entries が空(汎用メッセージ): scrollIntoView が呼ばれる', async () => {
|
|
422
|
+
vi.spyOn(globalThis, 'fetch').mockImplementation(() => Promise.resolve(new Response('{}', {
|
|
423
|
+
status: 500,
|
|
424
|
+
headers: { 'Content-Type': 'application/json' },
|
|
425
|
+
})));
|
|
426
|
+
const form = document.createElement('form');
|
|
427
|
+
const btn = document.createElement('button');
|
|
428
|
+
btn.type = 'button';
|
|
429
|
+
btn.setAttribute('data-click-fetch', 'http://api.test/json-empty-error');
|
|
430
|
+
btn.setAttribute('data-click-scroll-error', '');
|
|
431
|
+
form.appendChild(btn);
|
|
432
|
+
document.body.appendChild(form);
|
|
433
|
+
await waitForDomSettled();
|
|
434
|
+
btn.click();
|
|
435
|
+
await waitForCondition(() => form.getAttribute('data-message-level') === 'error', { description: 'general error message set (json empty)' });
|
|
436
|
+
expect(scrollIntoViewSpy).toHaveBeenCalledWith({
|
|
437
|
+
behavior: 'smooth',
|
|
438
|
+
block: 'nearest',
|
|
439
|
+
});
|
|
440
|
+
form.remove();
|
|
441
|
+
});
|
|
442
|
+
it('text/plain フォールバック: scrollIntoView が呼ばれる', async () => {
|
|
443
|
+
vi.spyOn(globalThis, 'fetch').mockImplementation(() => Promise.resolve(new Response('Internal Server Error', {
|
|
444
|
+
status: 500,
|
|
445
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
446
|
+
})));
|
|
447
|
+
const form = document.createElement('form');
|
|
448
|
+
const btn = document.createElement('button');
|
|
449
|
+
btn.type = 'button';
|
|
450
|
+
btn.setAttribute('data-click-fetch', 'http://api.test/text-error');
|
|
451
|
+
btn.setAttribute('data-click-scroll-error', '');
|
|
452
|
+
form.appendChild(btn);
|
|
453
|
+
document.body.appendChild(form);
|
|
454
|
+
await waitForDomSettled();
|
|
455
|
+
btn.click();
|
|
456
|
+
await waitForCondition(() => form.getAttribute('data-message-level') === 'error', { description: 'text fallback error message set' });
|
|
457
|
+
expect(scrollIntoViewSpy).toHaveBeenCalledWith({
|
|
458
|
+
behavior: 'smooth',
|
|
459
|
+
block: 'nearest',
|
|
460
|
+
});
|
|
461
|
+
form.remove();
|
|
462
|
+
});
|
|
463
|
+
it('non-form target: addErrorMessage が parentElement に付与したエラー要素へ scrollIntoView が呼ばれる', async () => {
|
|
464
|
+
vi.spyOn(globalThis, 'fetch').mockImplementation(() => Promise.resolve(new Response('Server Error', {
|
|
465
|
+
status: 500,
|
|
466
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
467
|
+
})));
|
|
468
|
+
// フォームではなく div の中にボタンを置く
|
|
469
|
+
const container = document.createElement('div');
|
|
470
|
+
const btn = document.createElement('button');
|
|
471
|
+
btn.type = 'button';
|
|
472
|
+
btn.setAttribute('data-click-fetch', 'http://api.test/non-form-error');
|
|
473
|
+
btn.setAttribute('data-click-scroll-error', '');
|
|
474
|
+
container.appendChild(btn);
|
|
475
|
+
document.body.appendChild(container);
|
|
476
|
+
await waitForDomSettled();
|
|
477
|
+
btn.click();
|
|
478
|
+
// addErrorMessage は非フォーム target の parentElement にエラーを付与する
|
|
479
|
+
await waitForCondition(() => container.getAttribute('data-message-level') === 'error', { description: 'error set on parentElement (container)' });
|
|
480
|
+
// container(btn.parentElement)に対して scrollIntoView が 1 回呼ばれる
|
|
481
|
+
expect(scrollIntoViewSpy).toHaveBeenCalledTimes(1);
|
|
482
|
+
expect(scrollIntoViewSpy.mock.instances[0]).toBe(container);
|
|
483
|
+
container.remove();
|
|
484
|
+
});
|
|
485
|
+
it('data-click-scroll-error がない場合は scrollIntoView が呼ばれない', async () => {
|
|
486
|
+
vi.spyOn(globalThis, 'fetch').mockImplementation(() => Promise.resolve(new Response('Error', {
|
|
487
|
+
status: 500,
|
|
488
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
489
|
+
})));
|
|
490
|
+
const form = document.createElement('form');
|
|
491
|
+
const btn = document.createElement('button');
|
|
492
|
+
btn.type = 'button';
|
|
493
|
+
btn.setAttribute('data-click-fetch', 'http://api.test/no-scroll');
|
|
494
|
+
form.appendChild(btn);
|
|
495
|
+
document.body.appendChild(form);
|
|
496
|
+
await waitForDomSettled();
|
|
497
|
+
btn.click();
|
|
498
|
+
await waitForCondition(() => form.getAttribute('data-message-level') === 'error', { description: 'error message set without scroll-error attr' });
|
|
499
|
+
expect(scrollIntoViewSpy).not.toHaveBeenCalled();
|
|
500
|
+
form.remove();
|
|
501
|
+
});
|
|
502
|
+
it('バリデーション失敗: scrollIntoView が呼ばれる', async () => {
|
|
503
|
+
const fetchSpy = vi.spyOn(globalThis, 'fetch');
|
|
504
|
+
const form = document.createElement('form');
|
|
505
|
+
const input = document.createElement('input');
|
|
506
|
+
input.name = 'name';
|
|
507
|
+
input.required = true;
|
|
508
|
+
input.value = '';
|
|
509
|
+
form.appendChild(input);
|
|
510
|
+
const btn = document.createElement('button');
|
|
511
|
+
btn.type = 'button';
|
|
512
|
+
btn.setAttribute('data-click-fetch', 'http://api.test/validate');
|
|
513
|
+
btn.setAttribute('data-click-form', '');
|
|
514
|
+
btn.setAttribute('data-click-validate', '');
|
|
515
|
+
btn.setAttribute('data-click-scroll-error', '');
|
|
516
|
+
form.appendChild(btn);
|
|
517
|
+
document.body.appendChild(form);
|
|
518
|
+
await waitForDomSettled();
|
|
519
|
+
btn.click();
|
|
520
|
+
await waitForCondition(() => scrollIntoViewSpy.mock.calls.length > 0, { description: 'scrollIntoView called on validation error' });
|
|
521
|
+
expect(scrollIntoViewSpy).toHaveBeenCalledWith({
|
|
522
|
+
behavior: 'smooth',
|
|
523
|
+
block: 'nearest',
|
|
524
|
+
});
|
|
525
|
+
expect(fetchSpy).not.toHaveBeenCalled();
|
|
526
|
+
form.remove();
|
|
527
|
+
});
|
|
528
|
+
it('バリデーション失敗(複数 invalid): scrollIntoView は先頭の invalid 要素に 1 回だけ呼ばれ、reportValidity も先頭要素に 1 回だけ呼ばれる', async () => {
|
|
529
|
+
// 検出フェーズは checkValidity で行われるため、checkValidity をスタブして両 input を invalid にする
|
|
530
|
+
vi.spyOn(HTMLInputElement.prototype, 'checkValidity').mockReturnValue(false);
|
|
531
|
+
// reportValidity は先頭 1 要素にだけ呼ばれることを確認するためスパイを立てる
|
|
532
|
+
const reportValiditySpy = vi
|
|
533
|
+
.spyOn(HTMLInputElement.prototype, 'reportValidity')
|
|
534
|
+
.mockReturnValue(false);
|
|
535
|
+
const form = document.createElement('form');
|
|
536
|
+
const inputA = document.createElement('input'); // DOM 上で先頭
|
|
537
|
+
inputA.name = 'name';
|
|
538
|
+
form.appendChild(inputA);
|
|
539
|
+
const inputB = document.createElement('input'); // DOM 上で 2 番目
|
|
540
|
+
inputB.name = 'email';
|
|
541
|
+
form.appendChild(inputB);
|
|
542
|
+
const btn = document.createElement('button');
|
|
543
|
+
btn.type = 'button';
|
|
544
|
+
btn.setAttribute('data-click-fetch', 'http://api.test/multi-validate');
|
|
545
|
+
btn.setAttribute('data-click-form', '');
|
|
546
|
+
btn.setAttribute('data-click-validate', '');
|
|
547
|
+
btn.setAttribute('data-click-scroll-error', '');
|
|
548
|
+
form.appendChild(btn);
|
|
549
|
+
document.body.appendChild(form);
|
|
550
|
+
await waitForDomSettled();
|
|
551
|
+
btn.click();
|
|
552
|
+
await waitForCondition(() => scrollIntoViewSpy.mock.calls.length > 0, { description: 'scrollIntoView called once on first invalid' });
|
|
553
|
+
// scrollIntoView は 1 回だけ、先頭の invalid 要素(inputA)に対して呼ばれる
|
|
554
|
+
expect(scrollIntoViewSpy).toHaveBeenCalledTimes(1);
|
|
555
|
+
expect(scrollIntoViewSpy.mock.instances[0]).toBe(inputA);
|
|
556
|
+
// reportValidity も先頭要素にだけ呼ばれる(複数の native UI が出ない)
|
|
557
|
+
expect(reportValiditySpy).toHaveBeenCalledTimes(1);
|
|
558
|
+
expect(reportValiditySpy.mock.instances[0]).toBe(inputA);
|
|
559
|
+
form.remove();
|
|
560
|
+
});
|
|
561
|
+
});
|
|
562
|
+
// -----------------------------------------------------------------------
|
|
563
|
+
// data-click-scroll
|
|
564
|
+
// -----------------------------------------------------------------------
|
|
565
|
+
describe('data-click-scroll', () => {
|
|
566
|
+
let scrollIntoViewSpy;
|
|
567
|
+
beforeEach(() => {
|
|
568
|
+
Element.prototype.scrollIntoView = () => { };
|
|
569
|
+
scrollIntoViewSpy = vi
|
|
570
|
+
.spyOn(Element.prototype, 'scrollIntoView')
|
|
571
|
+
.mockImplementation(() => { });
|
|
572
|
+
});
|
|
573
|
+
afterEach(() => {
|
|
574
|
+
delete Element.prototype
|
|
575
|
+
.scrollIntoView;
|
|
576
|
+
});
|
|
577
|
+
it('成功時にセレクターで指定した要素へ scrollIntoView が呼ばれる', async () => {
|
|
578
|
+
vi.spyOn(globalThis, 'fetch').mockImplementation(() => Promise.resolve(new Response('{}', { headers: { 'Content-Type': 'application/json' } })));
|
|
579
|
+
const target = document.createElement('div');
|
|
580
|
+
target.id = 'scroll-target';
|
|
581
|
+
document.body.appendChild(target);
|
|
582
|
+
const btn = document.createElement('button');
|
|
583
|
+
btn.type = 'button';
|
|
584
|
+
btn.setAttribute('data-click-fetch', 'http://api.test/success');
|
|
585
|
+
btn.setAttribute('data-click-scroll', '#scroll-target');
|
|
586
|
+
document.body.appendChild(btn);
|
|
587
|
+
await waitForDomSettled();
|
|
588
|
+
btn.click();
|
|
589
|
+
await waitForCondition(() => scrollIntoViewSpy.mock.calls.length > 0, { description: 'scrollIntoView called on success' });
|
|
590
|
+
expect(scrollIntoViewSpy).toHaveBeenCalledTimes(1);
|
|
591
|
+
expect(scrollIntoViewSpy.mock.instances[0]).toBe(target);
|
|
592
|
+
expect(scrollIntoViewSpy).toHaveBeenCalledWith({
|
|
593
|
+
behavior: 'smooth',
|
|
594
|
+
block: 'nearest',
|
|
595
|
+
});
|
|
596
|
+
target.remove();
|
|
597
|
+
btn.remove();
|
|
598
|
+
});
|
|
599
|
+
it('フェッチエラー時は scrollIntoView が呼ばれない', async () => {
|
|
600
|
+
vi.spyOn(globalThis, 'fetch').mockImplementation(() => Promise.resolve(new Response('Error', {
|
|
601
|
+
status: 500,
|
|
602
|
+
headers: { 'Content-Type': 'text/plain' },
|
|
603
|
+
})));
|
|
604
|
+
const target = document.createElement('div');
|
|
605
|
+
target.id = 'scroll-target-error';
|
|
606
|
+
document.body.appendChild(target);
|
|
607
|
+
const form = document.createElement('form');
|
|
608
|
+
const btn = document.createElement('button');
|
|
609
|
+
btn.type = 'button';
|
|
610
|
+
btn.setAttribute('data-click-fetch', 'http://api.test/fail');
|
|
611
|
+
btn.setAttribute('data-click-scroll', '#scroll-target-error');
|
|
612
|
+
form.appendChild(btn);
|
|
613
|
+
document.body.appendChild(form);
|
|
614
|
+
await waitForDomSettled();
|
|
615
|
+
btn.click();
|
|
616
|
+
await waitForCondition(() => form.getAttribute('data-message-level') === 'error', { description: 'error message set' });
|
|
617
|
+
expect(scrollIntoViewSpy).not.toHaveBeenCalled();
|
|
618
|
+
target.remove();
|
|
619
|
+
form.remove();
|
|
620
|
+
});
|
|
621
|
+
it('セレクターにマッチする要素がない場合は scrollIntoView が呼ばれない', async () => {
|
|
622
|
+
vi.spyOn(globalThis, 'fetch').mockImplementation(() => Promise.resolve(new Response('{}', { headers: { 'Content-Type': 'application/json' } })));
|
|
623
|
+
const btn = document.createElement('button');
|
|
624
|
+
btn.type = 'button';
|
|
625
|
+
btn.setAttribute('data-click-fetch', 'http://api.test/success-no-target');
|
|
626
|
+
btn.setAttribute('data-click-scroll', '#nonexistent');
|
|
627
|
+
document.body.appendChild(btn);
|
|
628
|
+
await waitForDomSettled();
|
|
629
|
+
btn.click();
|
|
630
|
+
// fetch 完了を待つ(scrollIntoView は呼ばれないのでタイムアウト前に別の条件で判断)
|
|
631
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
632
|
+
expect(scrollIntoViewSpy).not.toHaveBeenCalled();
|
|
633
|
+
btn.remove();
|
|
634
|
+
});
|
|
635
|
+
});
|
|
636
|
+
// -----------------------------------------------------------------------
|
|
381
637
|
// history.pushState
|
|
382
638
|
// -----------------------------------------------------------------------
|
|
383
639
|
it('data-click-history calls history.pushState with the specified URL', async () => {
|
|
@@ -396,7 +652,7 @@ describe('Procedure action operations', () => {
|
|
|
396
652
|
await waitForCondition(() => pushStateSpy.mock.calls.length > 0, {
|
|
397
653
|
description: 'pushState called',
|
|
398
654
|
});
|
|
399
|
-
expect(pushStateSpy).toHaveBeenCalledWith({}, '', expect.stringContaining('/new-path'));
|
|
655
|
+
expect(pushStateSpy).toHaveBeenCalledWith({ __haoriHistoryState__: true }, '', expect.stringContaining('/new-path'));
|
|
400
656
|
container.remove();
|
|
401
657
|
});
|
|
402
658
|
it('data-click-history-data appends query params to the URL', async () => {
|
|
@@ -416,7 +672,7 @@ describe('Procedure action operations', () => {
|
|
|
416
672
|
await waitForCondition(() => pushStateSpy.mock.calls.length > 0, {
|
|
417
673
|
description: 'pushState called with query params',
|
|
418
674
|
});
|
|
419
|
-
expect(pushStateSpy).toHaveBeenCalledWith({}, '', expect.stringMatching(/\/search\?.*keyword=hello/));
|
|
675
|
+
expect(pushStateSpy).toHaveBeenCalledWith({ __haoriHistoryState__: true }, '', expect.stringMatching(/\/search\?.*keyword=hello/));
|
|
420
676
|
container.remove();
|
|
421
677
|
});
|
|
422
678
|
it('data-click-history omitted with data only updates query on current path', async () => {
|
|
@@ -435,7 +691,7 @@ describe('Procedure action operations', () => {
|
|
|
435
691
|
await waitForCondition(() => pushStateSpy.mock.calls.length > 0, {
|
|
436
692
|
description: 'pushState called with current-path query update',
|
|
437
693
|
});
|
|
438
|
-
expect(pushStateSpy).toHaveBeenCalledWith({}, '', expect.stringMatching(/tab=list/));
|
|
694
|
+
expect(pushStateSpy).toHaveBeenCalledWith({ __haoriHistoryState__: true }, '', expect.stringMatching(/tab=list/));
|
|
439
695
|
container.remove();
|
|
440
696
|
});
|
|
441
697
|
it('data-click-history-form appends form values as query params', async () => {
|
|
@@ -462,7 +718,7 @@ describe('Procedure action operations', () => {
|
|
|
462
718
|
await waitForCondition(() => pushStateSpy.mock.calls.length > 0, {
|
|
463
719
|
description: 'pushState called with form values',
|
|
464
720
|
});
|
|
465
|
-
expect(pushStateSpy).toHaveBeenCalledWith({}, '', expect.stringMatching(/\/search\?.*q=vitest/));
|
|
721
|
+
expect(pushStateSpy).toHaveBeenCalledWith({ __haoriHistoryState__: true }, '', expect.stringMatching(/\/search\?.*q=vitest/));
|
|
466
722
|
container.remove();
|
|
467
723
|
});
|
|
468
724
|
it('data-click-history with cross-origin URL logs error and skips pushState', async () => {
|
|
@@ -511,4 +767,17 @@ describe('Procedure action operations', () => {
|
|
|
511
767
|
container.remove();
|
|
512
768
|
});
|
|
513
769
|
});
|
|
770
|
+
describe('popstate auto-reload', () => {
|
|
771
|
+
it('popstate イベント発火時に location.reload() を呼び出す', async () => {
|
|
772
|
+
const mockReload = vi.fn();
|
|
773
|
+
vi.stubGlobal('location', { reload: mockReload });
|
|
774
|
+
window.dispatchEvent(new PopStateEvent('popstate', { state: {} }));
|
|
775
|
+
expect(mockReload).not.toHaveBeenCalled();
|
|
776
|
+
window.dispatchEvent(new PopStateEvent('popstate', {
|
|
777
|
+
state: { __haoriHistoryState__: true },
|
|
778
|
+
}));
|
|
779
|
+
expect(mockReload).toHaveBeenCalledTimes(1);
|
|
780
|
+
vi.unstubAllGlobals();
|
|
781
|
+
});
|
|
782
|
+
});
|
|
514
783
|
//# sourceMappingURL=procedure-action-operations.test.js.map
|