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.
Files changed (55) hide show
  1. package/README.ja.md +4 -1
  2. package/README.md +4 -1
  3. package/dist/haori.cjs.js +12 -10
  4. package/dist/haori.cjs.js.map +1 -1
  5. package/dist/haori.es.js +997 -856
  6. package/dist/haori.es.js.map +1 -1
  7. package/dist/haori.iife.js +13 -11
  8. package/dist/haori.iife.js.map +1 -1
  9. package/dist/index.d.ts +66 -3
  10. package/dist/package.json +1 -1
  11. package/dist/src/core.d.ts +17 -0
  12. package/dist/src/core.d.ts.map +1 -1
  13. package/dist/src/core.js +75 -22
  14. package/dist/src/core.js.map +1 -1
  15. package/dist/src/event_dispatcher.d.ts +10 -2
  16. package/dist/src/event_dispatcher.d.ts.map +1 -1
  17. package/dist/src/event_dispatcher.js +21 -3
  18. package/dist/src/event_dispatcher.js.map +1 -1
  19. package/dist/src/form.d.ts +12 -1
  20. package/dist/src/form.d.ts.map +1 -1
  21. package/dist/src/form.js +20 -3
  22. package/dist/src/form.js.map +1 -1
  23. package/dist/src/fragment.d.ts +27 -0
  24. package/dist/src/fragment.d.ts.map +1 -1
  25. package/dist/src/fragment.js +62 -8
  26. package/dist/src/fragment.js.map +1 -1
  27. package/dist/src/haori.d.ts +10 -2
  28. package/dist/src/haori.d.ts.map +1 -1
  29. package/dist/src/haori.js +23 -11
  30. package/dist/src/haori.js.map +1 -1
  31. package/dist/src/observer.d.ts.map +1 -1
  32. package/dist/src/observer.js +4 -0
  33. package/dist/src/observer.js.map +1 -1
  34. package/dist/src/procedure.d.ts +18 -3
  35. package/dist/src/procedure.d.ts.map +1 -1
  36. package/dist/src/procedure.js +78 -27
  37. package/dist/src/procedure.js.map +1 -1
  38. package/dist/tests/core.test.js +40 -0
  39. package/dist/tests/core.test.js.map +1 -1
  40. package/dist/tests/fetch-and-procedure-scenarios.test.js +54 -0
  41. package/dist/tests/fetch-and-procedure-scenarios.test.js.map +1 -1
  42. package/dist/tests/haori.test.d.ts +2 -0
  43. package/dist/tests/haori.test.d.ts.map +1 -0
  44. package/dist/tests/haori.test.js +149 -0
  45. package/dist/tests/haori.test.js.map +1 -0
  46. package/dist/tests/helpers/async.d.ts.map +1 -1
  47. package/dist/tests/helpers/async.js +1 -0
  48. package/dist/tests/helpers/async.js.map +1 -1
  49. package/dist/tests/procedure-action-operations.test.js +273 -4
  50. package/dist/tests/procedure-action-operations.test.js.map +1 -1
  51. package/dist/tests/procedure.test.js +3 -2
  52. package/dist/tests/procedure.test.js.map +1 -1
  53. package/dist/tests/row-operations.test.js +21 -1
  54. package/dist/tests/row-operations.test.js.map +1 -1
  55. 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,CAKjE;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
+ {"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"}
@@ -8,6 +8,7 @@ import Queue from '../../src/queue';
8
8
  export async function waitForDomSettled(cycles = 3) {
9
9
  for (let index = 0; index < cycles; index += 1) {
10
10
  await Promise.resolve();
11
+ await new Promise(resolve => setTimeout(resolve, 0));
11
12
  await Queue.wait();
12
13
  }
13
14
  }
@@ -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