haori 0.13.0 → 0.13.2
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 +1 -1
- package/README.md +1 -1
- package/dist/haori.cjs.js +10 -10
- package/dist/haori.es.js +191 -165
- package/dist/haori.iife.js +10 -10
- package/dist/index.d.ts +1 -1
- package/package.json +11 -3
- package/dist/haori.cjs.js.map +0 -1
- package/dist/haori.es.js.map +0 -1
- package/dist/haori.iife.js.map +0 -1
- package/dist/package.json +0 -70
- package/dist/src/builtins.d.ts +0 -100
- package/dist/src/builtins.d.ts.map +0 -1
- package/dist/src/builtins.js +0 -194
- package/dist/src/builtins.js.map +0 -1
- package/dist/src/core.d.ts +0 -505
- package/dist/src/core.d.ts.map +0 -1
- package/dist/src/core.js +0 -1817
- package/dist/src/core.js.map +0 -1
- package/dist/src/dev.d.ts +0 -35
- package/dist/src/dev.d.ts.map +0 -1
- package/dist/src/dev.js +0 -44
- package/dist/src/dev.js.map +0 -1
- package/dist/src/env.d.ts +0 -43
- package/dist/src/env.d.ts.map +0 -1
- package/dist/src/env.js +0 -108
- package/dist/src/env.js.map +0 -1
- package/dist/src/event.d.ts +0 -171
- package/dist/src/event.d.ts.map +0 -1
- package/dist/src/event.js +0 -235
- package/dist/src/event.js.map +0 -1
- package/dist/src/event_dispatcher.d.ts +0 -66
- package/dist/src/event_dispatcher.d.ts.map +0 -1
- package/dist/src/event_dispatcher.js +0 -167
- package/dist/src/event_dispatcher.js.map +0 -1
- package/dist/src/expression.d.ts +0 -260
- package/dist/src/expression.d.ts.map +0 -1
- package/dist/src/expression.js +0 -975
- package/dist/src/expression.js.map +0 -1
- package/dist/src/form.d.ts +0 -148
- package/dist/src/form.d.ts.map +0 -1
- package/dist/src/form.js +0 -428
- package/dist/src/form.js.map +0 -1
- package/dist/src/fragment.d.ts +0 -621
- package/dist/src/fragment.d.ts.map +0 -1
- package/dist/src/fragment.js +0 -1963
- package/dist/src/fragment.js.map +0 -1
- package/dist/src/haori.d.ts +0 -130
- package/dist/src/haori.d.ts.map +0 -1
- package/dist/src/haori.js +0 -215
- package/dist/src/haori.js.map +0 -1
- package/dist/src/import.d.ts +0 -19
- package/dist/src/import.d.ts.map +0 -1
- package/dist/src/import.js +0 -64
- package/dist/src/import.js.map +0 -1
- package/dist/src/index.d.ts +0 -27
- package/dist/src/index.d.ts.map +0 -1
- package/dist/src/index.js +0 -30
- package/dist/src/index.js.map +0 -1
- package/dist/src/intersect.d.ts +0 -28
- package/dist/src/intersect.d.ts.map +0 -1
- package/dist/src/intersect.js +0 -201
- package/dist/src/intersect.js.map +0 -1
- package/dist/src/log.d.ts +0 -32
- package/dist/src/log.d.ts.map +0 -1
- package/dist/src/log.js +0 -43
- package/dist/src/log.js.map +0 -1
- package/dist/src/observer.d.ts +0 -24
- package/dist/src/observer.d.ts.map +0 -1
- package/dist/src/observer.js +0 -131
- package/dist/src/observer.js.map +0 -1
- package/dist/src/procedure.d.ts +0 -447
- package/dist/src/procedure.d.ts.map +0 -1
- package/dist/src/procedure.js +0 -1921
- package/dist/src/procedure.js.map +0 -1
- package/dist/src/queue.d.ts +0 -35
- package/dist/src/queue.d.ts.map +0 -1
- package/dist/src/queue.js +0 -202
- package/dist/src/queue.js.map +0 -1
- package/dist/src/url.d.ts +0 -14
- package/dist/src/url.d.ts.map +0 -1
- package/dist/src/url.js +0 -22
- package/dist/src/url.js.map +0 -1
- package/dist/tests/bind-arg-own-binding.test.d.ts +0 -2
- package/dist/tests/bind-arg-own-binding.test.d.ts.map +0 -1
- package/dist/tests/bind-arg-own-binding.test.js +0 -63
- package/dist/tests/bind-arg-own-binding.test.js.map +0 -1
- package/dist/tests/bindcomplete-and-scope.test.d.ts +0 -2
- package/dist/tests/bindcomplete-and-scope.test.d.ts.map +0 -1
- package/dist/tests/bindcomplete-and-scope.test.js +0 -115
- package/dist/tests/bindcomplete-and-scope.test.js.map +0 -1
- package/dist/tests/builtins.test.d.ts +0 -2
- package/dist/tests/builtins.test.d.ts.map +0 -1
- package/dist/tests/builtins.test.js +0 -152
- package/dist/tests/builtins.test.js.map +0 -1
- package/dist/tests/click-attributes.test.d.ts +0 -2
- package/dist/tests/click-attributes.test.d.ts.map +0 -1
- package/dist/tests/click-attributes.test.js +0 -113
- package/dist/tests/click-attributes.test.js.map +0 -1
- package/dist/tests/click-no-disabled.test.d.ts +0 -2
- package/dist/tests/click-no-disabled.test.d.ts.map +0 -1
- package/dist/tests/click-no-disabled.test.js +0 -67
- package/dist/tests/click-no-disabled.test.js.map +0 -1
- package/dist/tests/core.test.d.ts +0 -5
- package/dist/tests/core.test.d.ts.map +0 -1
- package/dist/tests/core.test.js +0 -1056
- package/dist/tests/core.test.js.map +0 -1
- package/dist/tests/data-bind-arg-reeval.test.d.ts +0 -2
- package/dist/tests/data-bind-arg-reeval.test.d.ts.map +0 -1
- package/dist/tests/data-bind-arg-reeval.test.js +0 -119
- package/dist/tests/data-bind-arg-reeval.test.js.map +0 -1
- package/dist/tests/data-bind-merge.test.d.ts +0 -2
- package/dist/tests/data-bind-merge.test.d.ts.map +0 -1
- package/dist/tests/data-bind-merge.test.js +0 -86
- package/dist/tests/data-bind-merge.test.js.map +0 -1
- package/dist/tests/data-click-defer.test.d.ts +0 -2
- package/dist/tests/data-click-defer.test.d.ts.map +0 -1
- package/dist/tests/data-click-defer.test.js +0 -57
- package/dist/tests/data-click-defer.test.js.map +0 -1
- package/dist/tests/data-click-run.test.d.ts +0 -5
- package/dist/tests/data-click-run.test.d.ts.map +0 -1
- package/dist/tests/data-click-run.test.js +0 -109
- package/dist/tests/data-click-run.test.js.map +0 -1
- package/dist/tests/data-derive.test.d.ts +0 -2
- package/dist/tests/data-derive.test.d.ts.map +0 -1
- package/dist/tests/data-derive.test.js +0 -999
- package/dist/tests/data-derive.test.js.map +0 -1
- package/dist/tests/data-each-browserlike.test.d.ts +0 -2
- package/dist/tests/data-each-browserlike.test.d.ts.map +0 -1
- package/dist/tests/data-each-browserlike.test.js +0 -48
- package/dist/tests/data-each-browserlike.test.js.map +0 -1
- package/dist/tests/data-each-fragment-debug.test.d.ts +0 -2
- package/dist/tests/data-each-fragment-debug.test.d.ts.map +0 -1
- package/dist/tests/data-each-fragment-debug.test.js +0 -119
- package/dist/tests/data-each-fragment-debug.test.js.map +0 -1
- package/dist/tests/data-each-table.test.d.ts +0 -2
- package/dist/tests/data-each-table.test.d.ts.map +0 -1
- package/dist/tests/data-each-table.test.js +0 -131
- package/dist/tests/data-each-table.test.js.map +0 -1
- package/dist/tests/data-each-template-recovery.test.d.ts +0 -2
- package/dist/tests/data-each-template-recovery.test.d.ts.map +0 -1
- package/dist/tests/data-each-template-recovery.test.js +0 -87
- package/dist/tests/data-each-template-recovery.test.js.map +0 -1
- package/dist/tests/data-event-prevent.test.d.ts +0 -2
- package/dist/tests/data-event-prevent.test.d.ts.map +0 -1
- package/dist/tests/data-event-prevent.test.js +0 -76
- package/dist/tests/data-event-prevent.test.js.map +0 -1
- package/dist/tests/data-fetch-form-input-binding.test.d.ts +0 -2
- package/dist/tests/data-fetch-form-input-binding.test.d.ts.map +0 -1
- package/dist/tests/data-fetch-form-input-binding.test.js +0 -202
- package/dist/tests/data-fetch-form-input-binding.test.js.map +0 -1
- package/dist/tests/data-fetch-tbody-dom.test.d.ts +0 -2
- package/dist/tests/data-fetch-tbody-dom.test.d.ts.map +0 -1
- package/dist/tests/data-fetch-tbody-dom.test.js +0 -82
- package/dist/tests/data-fetch-tbody-dom.test.js.map +0 -1
- package/dist/tests/data-form-container.test.d.ts +0 -2
- package/dist/tests/data-form-container.test.d.ts.map +0 -1
- package/dist/tests/data-form-container.test.js +0 -88
- package/dist/tests/data-form-container.test.js.map +0 -1
- package/dist/tests/data-if-dev-diagnostics.test.d.ts +0 -2
- package/dist/tests/data-if-dev-diagnostics.test.d.ts.map +0 -1
- package/dist/tests/data-if-dev-diagnostics.test.js +0 -55
- package/dist/tests/data-if-dev-diagnostics.test.js.map +0 -1
- package/dist/tests/data-if-falsy.test.d.ts +0 -2
- package/dist/tests/data-if-falsy.test.d.ts.map +0 -1
- package/dist/tests/data-if-falsy.test.js +0 -73
- package/dist/tests/data-if-falsy.test.js.map +0 -1
- package/dist/tests/data-load-on-show.test.d.ts +0 -2
- package/dist/tests/data-load-on-show.test.d.ts.map +0 -1
- package/dist/tests/data-load-on-show.test.js +0 -98
- package/dist/tests/data-load-on-show.test.js.map +0 -1
- package/dist/tests/derive-each-render-wait.test.d.ts +0 -2
- package/dist/tests/derive-each-render-wait.test.d.ts.map +0 -1
- package/dist/tests/derive-each-render-wait.test.js +0 -90
- package/dist/tests/derive-each-render-wait.test.js.map +0 -1
- package/dist/tests/dev.test.d.ts +0 -2
- package/dist/tests/dev.test.d.ts.map +0 -1
- package/dist/tests/dev.test.js +0 -51
- package/dist/tests/dev.test.js.map +0 -1
- package/dist/tests/each-update-event.test.d.ts +0 -2
- package/dist/tests/each-update-event.test.d.ts.map +0 -1
- package/dist/tests/each-update-event.test.js +0 -83
- package/dist/tests/each-update-event.test.js.map +0 -1
- package/dist/tests/each_arg.test.d.ts +0 -2
- package/dist/tests/each_arg.test.d.ts.map +0 -1
- package/dist/tests/each_arg.test.js +0 -41
- package/dist/tests/each_arg.test.js.map +0 -1
- package/dist/tests/env.test.d.ts +0 -2
- package/dist/tests/env.test.d.ts.map +0 -1
- package/dist/tests/env.test.js +0 -124
- package/dist/tests/env.test.js.map +0 -1
- package/dist/tests/evaluation-profile.test.d.ts +0 -2
- package/dist/tests/evaluation-profile.test.d.ts.map +0 -1
- package/dist/tests/evaluation-profile.test.js +0 -92
- package/dist/tests/evaluation-profile.test.js.map +0 -1
- package/dist/tests/event.test.d.ts +0 -2
- package/dist/tests/event.test.d.ts.map +0 -1
- package/dist/tests/event.test.js +0 -300
- package/dist/tests/event.test.js.map +0 -1
- package/dist/tests/expression-disallowed-keyword-hint.test.d.ts +0 -2
- package/dist/tests/expression-disallowed-keyword-hint.test.d.ts.map +0 -1
- package/dist/tests/expression-disallowed-keyword-hint.test.js +0 -45
- package/dist/tests/expression-disallowed-keyword-hint.test.js.map +0 -1
- package/dist/tests/expression-forbidden-hint.test.d.ts +0 -2
- package/dist/tests/expression-forbidden-hint.test.d.ts.map +0 -1
- package/dist/tests/expression-forbidden-hint.test.js +0 -37
- package/dist/tests/expression-forbidden-hint.test.js.map +0 -1
- package/dist/tests/expression.test.d.ts +0 -2
- package/dist/tests/expression.test.d.ts.map +0 -1
- package/dist/tests/expression.test.js +0 -482
- package/dist/tests/expression.test.js.map +0 -1
- package/dist/tests/fetch-and-procedure-scenarios.test.d.ts +0 -2
- package/dist/tests/fetch-and-procedure-scenarios.test.d.ts.map +0 -1
- package/dist/tests/fetch-and-procedure-scenarios.test.js +0 -195
- package/dist/tests/fetch-and-procedure-scenarios.test.js.map +0 -1
- package/dist/tests/form-number-type.test.d.ts +0 -2
- package/dist/tests/form-number-type.test.d.ts.map +0 -1
- package/dist/tests/form-number-type.test.js +0 -83
- package/dist/tests/form-number-type.test.js.map +0 -1
- package/dist/tests/form.test.d.ts +0 -2
- package/dist/tests/form.test.d.ts.map +0 -1
- package/dist/tests/form.test.js +0 -636
- package/dist/tests/form.test.js.map +0 -1
- package/dist/tests/fragment.test.d.ts +0 -2
- package/dist/tests/fragment.test.d.ts.map +0 -1
- package/dist/tests/fragment.test.js +0 -284
- package/dist/tests/fragment.test.js.map +0 -1
- package/dist/tests/haori.test.d.ts +0 -2
- package/dist/tests/haori.test.d.ts.map +0 -1
- package/dist/tests/haori.test.js +0 -149
- package/dist/tests/haori.test.js.map +0 -1
- package/dist/tests/helpers/async.d.ts +0 -20
- package/dist/tests/helpers/async.d.ts.map +0 -1
- package/dist/tests/helpers/async.js +0 -35
- package/dist/tests/helpers/async.js.map +0 -1
- package/dist/tests/import.test.d.ts +0 -2
- package/dist/tests/import.test.d.ts.map +0 -1
- package/dist/tests/import.test.js +0 -148
- package/dist/tests/import.test.js.map +0 -1
- package/dist/tests/intersect.test.d.ts +0 -2
- package/dist/tests/intersect.test.d.ts.map +0 -1
- package/dist/tests/intersect.test.js +0 -173
- package/dist/tests/intersect.test.js.map +0 -1
- package/dist/tests/log.test.d.ts +0 -2
- package/dist/tests/log.test.d.ts.map +0 -1
- package/dist/tests/log.test.js +0 -58
- package/dist/tests/log.test.js.map +0 -1
- package/dist/tests/observer.test.d.ts +0 -2
- package/dist/tests/observer.test.d.ts.map +0 -1
- package/dist/tests/observer.test.js +0 -64
- package/dist/tests/observer.test.js.map +0 -1
- package/dist/tests/placeholder-resolution.test.d.ts +0 -2
- package/dist/tests/placeholder-resolution.test.d.ts.map +0 -1
- package/dist/tests/placeholder-resolution.test.js +0 -463
- package/dist/tests/placeholder-resolution.test.js.map +0 -1
- package/dist/tests/procedure-action-operations.test.d.ts +0 -2
- package/dist/tests/procedure-action-operations.test.d.ts.map +0 -1
- package/dist/tests/procedure-action-operations.test.js +0 -1140
- package/dist/tests/procedure-action-operations.test.js.map +0 -1
- package/dist/tests/procedure-bind-append.test.d.ts +0 -2
- package/dist/tests/procedure-bind-append.test.d.ts.map +0 -1
- package/dist/tests/procedure-bind-append.test.js +0 -80
- package/dist/tests/procedure-bind-append.test.js.map +0 -1
- package/dist/tests/procedure-fetch-options.test.d.ts +0 -2
- package/dist/tests/procedure-fetch-options.test.d.ts.map +0 -1
- package/dist/tests/procedure-fetch-options.test.js +0 -255
- package/dist/tests/procedure-fetch-options.test.js.map +0 -1
- package/dist/tests/procedure.test.d.ts +0 -2
- package/dist/tests/procedure.test.d.ts.map +0 -1
- package/dist/tests/procedure.test.js +0 -137
- package/dist/tests/procedure.test.js.map +0 -1
- package/dist/tests/procedure_events.test.d.ts +0 -7
- package/dist/tests/procedure_events.test.d.ts.map +0 -1
- package/dist/tests/procedure_events.test.js +0 -261
- package/dist/tests/procedure_events.test.js.map +0 -1
- package/dist/tests/render-wait.test.d.ts +0 -2
- package/dist/tests/render-wait.test.d.ts.map +0 -1
- package/dist/tests/render-wait.test.js +0 -68
- package/dist/tests/render-wait.test.js.map +0 -1
- package/dist/tests/reset_each.test.d.ts +0 -2
- package/dist/tests/reset_each.test.d.ts.map +0 -1
- package/dist/tests/reset_each.test.js +0 -215
- package/dist/tests/reset_each.test.js.map +0 -1
- package/dist/tests/row-move.test.d.ts +0 -2
- package/dist/tests/row-move.test.d.ts.map +0 -1
- package/dist/tests/row-move.test.js +0 -220
- package/dist/tests/row-move.test.js.map +0 -1
- package/dist/tests/row-operations.test.d.ts +0 -2
- package/dist/tests/row-operations.test.d.ts.map +0 -1
- package/dist/tests/row-operations.test.js +0 -259
- package/dist/tests/row-operations.test.js.map +0 -1
- package/dist/tests/scheduled-evaluate-wait.test.d.ts +0 -2
- package/dist/tests/scheduled-evaluate-wait.test.d.ts.map +0 -1
- package/dist/tests/scheduled-evaluate-wait.test.js +0 -68
- package/dist/tests/scheduled-evaluate-wait.test.js.map +0 -1
- package/dist/tests/url.test.d.ts +0 -2
- package/dist/tests/url.test.d.ts.map +0 -1
- package/dist/tests/url.test.js +0 -150
- package/dist/tests/url.test.js.map +0 -1
- package/dist/vite.config.d.ts +0 -3
- package/dist/vite.config.d.ts.map +0 -1
- package/dist/vite.config.js +0 -28
- package/dist/vite.config.js.map +0 -1
- package/dist/vitest.config.d.ts +0 -3
- package/dist/vitest.config.d.ts.map +0 -1
- package/dist/vitest.config.js +0 -19
- package/dist/vitest.config.js.map +0 -1
package/dist/tests/core.test.js
DELETED
|
@@ -1,1056 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @fileoverview Coreクラスのテスト
|
|
3
|
-
*/
|
|
4
|
-
import { vi } from 'vitest';
|
|
5
|
-
import Core from '../src/core';
|
|
6
|
-
import Fragment, { ElementFragment } from '../src/fragment';
|
|
7
|
-
import { waitForCondition, waitForDomSettled } from './helpers/async';
|
|
8
|
-
describe('Core', () => {
|
|
9
|
-
let container;
|
|
10
|
-
let observer;
|
|
11
|
-
let mutations;
|
|
12
|
-
beforeEach(() => {
|
|
13
|
-
container = document.createElement('div');
|
|
14
|
-
document.body.appendChild(container);
|
|
15
|
-
mutations = [];
|
|
16
|
-
observer = new MutationObserver(mutationsList => {
|
|
17
|
-
mutations.push(...mutationsList);
|
|
18
|
-
});
|
|
19
|
-
observer.observe(container, {
|
|
20
|
-
attributes: true,
|
|
21
|
-
childList: true,
|
|
22
|
-
subtree: true,
|
|
23
|
-
attributeOldValue: true,
|
|
24
|
-
});
|
|
25
|
-
});
|
|
26
|
-
afterEach(() => {
|
|
27
|
-
observer.disconnect();
|
|
28
|
-
document.body.removeChild(container);
|
|
29
|
-
mutations = [];
|
|
30
|
-
});
|
|
31
|
-
describe('evaluateAll', () => {
|
|
32
|
-
test('observerを通じてフォーム要素を処理する', async () => {
|
|
33
|
-
container.innerHTML = `
|
|
34
|
-
<form>
|
|
35
|
-
<input name="username" value="initial">
|
|
36
|
-
<input name="email" value="test@example.com">
|
|
37
|
-
</form>
|
|
38
|
-
`;
|
|
39
|
-
const form = container.querySelector('form');
|
|
40
|
-
const fragment = new ElementFragment(form);
|
|
41
|
-
// フォーム値を変更
|
|
42
|
-
const usernameInput = form.querySelector('[name="username"]');
|
|
43
|
-
usernameInput.value = 'updated_username';
|
|
44
|
-
Core.evaluateAll(fragment);
|
|
45
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
46
|
-
// MutationObserverが属性変更を検知していることを確認
|
|
47
|
-
const attributeMutations = mutations.filter(mutation => mutation.type === 'attributes');
|
|
48
|
-
expect(attributeMutations.length).toBeGreaterThanOrEqual(0);
|
|
49
|
-
});
|
|
50
|
-
test('observerを通じてdata-bind属性を処理する', async () => {
|
|
51
|
-
container.innerHTML = `
|
|
52
|
-
<div data-bind="testValue">
|
|
53
|
-
Initial content
|
|
54
|
-
</div>
|
|
55
|
-
`;
|
|
56
|
-
const element = container.querySelector('div');
|
|
57
|
-
const fragment = new ElementFragment(element);
|
|
58
|
-
Core.evaluateAll(fragment);
|
|
59
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
60
|
-
// data-bind属性の処理による変更を確認
|
|
61
|
-
const mutations_count = mutations.length;
|
|
62
|
-
expect(mutations_count).toBeGreaterThanOrEqual(0);
|
|
63
|
-
});
|
|
64
|
-
test('observerを通じてフォームオブジェクト構造を処理する', async () => {
|
|
65
|
-
container.innerHTML = `
|
|
66
|
-
<form>
|
|
67
|
-
<div data-haori-form-object="user">
|
|
68
|
-
<input name="name" value="John">
|
|
69
|
-
<input name="age" value="30">
|
|
70
|
-
</div>
|
|
71
|
-
</form>
|
|
72
|
-
`;
|
|
73
|
-
const form = container.querySelector('form');
|
|
74
|
-
const fragment = new ElementFragment(form);
|
|
75
|
-
Core.evaluateAll(fragment);
|
|
76
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
77
|
-
// フォーム構造の処理による変更を確認
|
|
78
|
-
expect(mutations.length).toBeGreaterThanOrEqual(0);
|
|
79
|
-
});
|
|
80
|
-
test('observerを通じてフォームリスト構造を処理する', async () => {
|
|
81
|
-
container.innerHTML = `
|
|
82
|
-
<form>
|
|
83
|
-
<div data-haori-form-list="items">
|
|
84
|
-
<div data-haori-row="0">
|
|
85
|
-
<input name="name" value="Item 1">
|
|
86
|
-
</div>
|
|
87
|
-
<div data-haori-row="1">
|
|
88
|
-
<input name="name" value="Item 2">
|
|
89
|
-
</div>
|
|
90
|
-
</div>
|
|
91
|
-
</form>
|
|
92
|
-
`;
|
|
93
|
-
const form = container.querySelector('form');
|
|
94
|
-
const fragment = new ElementFragment(form);
|
|
95
|
-
Core.evaluateAll(fragment);
|
|
96
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
97
|
-
// リスト構造の処理による変更を確認
|
|
98
|
-
expect(mutations.length).toBeGreaterThanOrEqual(0);
|
|
99
|
-
});
|
|
100
|
-
test('observerを通じてフォームdetach属性を処理する', async () => {
|
|
101
|
-
container.innerHTML = `
|
|
102
|
-
<form>
|
|
103
|
-
<input name="attached" value="attached_value">
|
|
104
|
-
<input name="detached" value="detached_value"
|
|
105
|
-
data-haori-form-detach="true">
|
|
106
|
-
</form>
|
|
107
|
-
`;
|
|
108
|
-
const form = container.querySelector('form');
|
|
109
|
-
const fragment = new ElementFragment(form);
|
|
110
|
-
Core.evaluateAll(fragment);
|
|
111
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
112
|
-
// data-haori-form-detach属性の処理による変更を確認
|
|
113
|
-
expect(mutations.length).toBeGreaterThanOrEqual(0);
|
|
114
|
-
});
|
|
115
|
-
test('observerを通じて複数のCore.evaluateAll呼び出しを処理する', async () => {
|
|
116
|
-
container.innerHTML = `
|
|
117
|
-
<form>
|
|
118
|
-
<input name="counter" value="0">
|
|
119
|
-
</form>
|
|
120
|
-
`;
|
|
121
|
-
const form = container.querySelector('form');
|
|
122
|
-
const fragment = new ElementFragment(form);
|
|
123
|
-
const input = form.querySelector('[name="counter"]');
|
|
124
|
-
// 最初の評価
|
|
125
|
-
Core.evaluateAll(fragment);
|
|
126
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
127
|
-
const firstMutationCount = mutations.length;
|
|
128
|
-
// 値を変更して再評価
|
|
129
|
-
input.value = '1';
|
|
130
|
-
Core.evaluateAll(fragment);
|
|
131
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
132
|
-
// 複数回の評価による変更を確認
|
|
133
|
-
expect(mutations.length).toBeGreaterThanOrEqual(firstMutationCount);
|
|
134
|
-
});
|
|
135
|
-
test('observerを通じてネストしたフォーム要素を処理する', async () => {
|
|
136
|
-
container.innerHTML = `
|
|
137
|
-
<form>
|
|
138
|
-
<div data-haori-form-object="parent">
|
|
139
|
-
<input name="parentName" value="Parent">
|
|
140
|
-
<div data-haori-form-object="child">
|
|
141
|
-
<input name="childName" value="Child">
|
|
142
|
-
</div>
|
|
143
|
-
</div>
|
|
144
|
-
</form>
|
|
145
|
-
`;
|
|
146
|
-
const form = container.querySelector('form');
|
|
147
|
-
const fragment = new ElementFragment(form);
|
|
148
|
-
Core.evaluateAll(fragment);
|
|
149
|
-
await new Promise(resolve => setTimeout(resolve, 10));
|
|
150
|
-
// ネストした要素の処理による変更を確認
|
|
151
|
-
expect(mutations.length).toBeGreaterThanOrEqual(0);
|
|
152
|
-
// 実際の要素が存在することを確認
|
|
153
|
-
const parentInput = form.querySelector('[name="parentName"]');
|
|
154
|
-
const childInput = form.querySelector('[name="childName"]');
|
|
155
|
-
expect(parentInput).toBeTruthy();
|
|
156
|
-
expect(childInput).toBeTruthy();
|
|
157
|
-
});
|
|
158
|
-
test('setBindingData で {{...}} を含む通常属性が再評価される', async () => {
|
|
159
|
-
container.innerHTML = `
|
|
160
|
-
<div data-bind='{"state":"before"}'>
|
|
161
|
-
<span class="{{state}}">label</span>
|
|
162
|
-
</div>
|
|
163
|
-
`;
|
|
164
|
-
const root = container.querySelector('div');
|
|
165
|
-
const target = root.querySelector('span');
|
|
166
|
-
await Core.scan(root);
|
|
167
|
-
// 初回評価で通常属性が DOM に反映されること。
|
|
168
|
-
expect(target.getAttribute('class')).toBe('before');
|
|
169
|
-
await Core.setBindingData(root, { state: 'after' });
|
|
170
|
-
// 再評価でも属性キャッシュと DOM が同じ値に更新されること。
|
|
171
|
-
expect(target.getAttribute('class')).toBe('after');
|
|
172
|
-
});
|
|
173
|
-
test('data-each 配下でも {{...}} を含む通常属性が再評価される', async () => {
|
|
174
|
-
container.innerHTML = `
|
|
175
|
-
<div
|
|
176
|
-
data-bind='{"items":[{"state":"hot"},{"state":"cold"}]}'
|
|
177
|
-
>
|
|
178
|
-
<ul data-each="items" data-each-arg="item">
|
|
179
|
-
<li class="{{item.state}}">{{item.state}}</li>
|
|
180
|
-
</ul>
|
|
181
|
-
</div>
|
|
182
|
-
`;
|
|
183
|
-
const root = container.querySelector('div');
|
|
184
|
-
const list = root.querySelector('ul');
|
|
185
|
-
await Core.scan(root);
|
|
186
|
-
// 初回の行生成で通常属性が各行に反映されること。
|
|
187
|
-
expect(Array.from(list.querySelectorAll('li')).map(item => item.className)).toEqual(['hot', 'cold']);
|
|
188
|
-
await Core.setBindingData(root, {
|
|
189
|
-
items: [{ state: 'warm' }, { state: 'cool' }],
|
|
190
|
-
});
|
|
191
|
-
// 既存行の再利用時も通常属性が再評価されること。
|
|
192
|
-
expect(Array.from(list.querySelectorAll('li')).map(item => item.className)).toEqual(['warm', 'cool']);
|
|
193
|
-
});
|
|
194
|
-
test('同じ行データなら data-each の再利用行を再評価しない', async () => {
|
|
195
|
-
container.innerHTML = `
|
|
196
|
-
<div id="root">
|
|
197
|
-
<p id="status">{{status}}</p>
|
|
198
|
-
<ul data-each="items" data-each-key="id" data-each-arg="item">
|
|
199
|
-
<li>{{renderSpy(item)}}</li>
|
|
200
|
-
</ul>
|
|
201
|
-
</div>
|
|
202
|
-
`;
|
|
203
|
-
const root = container.querySelector('#root');
|
|
204
|
-
const renderSpy = vi.fn((item) => item.label);
|
|
205
|
-
const items = [
|
|
206
|
-
{ id: 'a', label: 'alpha' },
|
|
207
|
-
{ id: 'b', label: 'beta' },
|
|
208
|
-
];
|
|
209
|
-
await Core.scan(root);
|
|
210
|
-
await Core.setBindingData(root, { items, status: 'before', renderSpy });
|
|
211
|
-
await waitForDomSettled();
|
|
212
|
-
await new Promise(resolve => setTimeout(resolve, 150));
|
|
213
|
-
await waitForDomSettled();
|
|
214
|
-
const baselineCalls = renderSpy.mock.calls.length;
|
|
215
|
-
expect(baselineCalls).toBeGreaterThanOrEqual(2);
|
|
216
|
-
await Core.setBindingData(root, { items, status: 'after', renderSpy });
|
|
217
|
-
await waitForDomSettled();
|
|
218
|
-
expect(renderSpy).toHaveBeenCalledTimes(baselineCalls);
|
|
219
|
-
expect(container.querySelector('#status')?.textContent).toBe('after');
|
|
220
|
-
expect(Array.from(container.querySelectorAll('li')).map(item => item.textContent)).toEqual(['alpha', 'beta']);
|
|
221
|
-
});
|
|
222
|
-
test('同値の新しい配列なら data-each 全体の差分更新をスキップする', async () => {
|
|
223
|
-
container.innerHTML = `
|
|
224
|
-
<div id="root">
|
|
225
|
-
<p id="status">{{status}}</p>
|
|
226
|
-
<ul data-each="items" data-each-key="id" data-each-arg="item">
|
|
227
|
-
<li>{{item.label}}</li>
|
|
228
|
-
</ul>
|
|
229
|
-
</div>
|
|
230
|
-
`;
|
|
231
|
-
const root = container.querySelector('#root');
|
|
232
|
-
await Core.scan(root);
|
|
233
|
-
await Core.setBindingData(root, {
|
|
234
|
-
items: [
|
|
235
|
-
{ id: 'a', label: 'alpha' },
|
|
236
|
-
{ id: 'b', label: 'beta' },
|
|
237
|
-
],
|
|
238
|
-
status: 'before',
|
|
239
|
-
});
|
|
240
|
-
await waitForDomSettled();
|
|
241
|
-
const updateDiffSpy = vi.spyOn(Core, 'updateDiff');
|
|
242
|
-
await Core.setBindingData(root, {
|
|
243
|
-
items: [
|
|
244
|
-
{ id: 'a', label: 'alpha' },
|
|
245
|
-
{ id: 'b', label: 'beta' },
|
|
246
|
-
],
|
|
247
|
-
status: 'after',
|
|
248
|
-
});
|
|
249
|
-
await waitForDomSettled();
|
|
250
|
-
expect(updateDiffSpy).not.toHaveBeenCalled();
|
|
251
|
-
expect(container.querySelector('#status')?.textContent).toBe('after');
|
|
252
|
-
expect(Array.from(container.querySelectorAll('li')).map(item => item.textContent)).toEqual(['alpha', 'beta']);
|
|
253
|
-
updateDiffSpy.mockRestore();
|
|
254
|
-
});
|
|
255
|
-
test('data-each の並び順変更では eachupdate が新しいキー順を通知する', async () => {
|
|
256
|
-
container.innerHTML = `
|
|
257
|
-
<div id="root">
|
|
258
|
-
<ul data-each="items" data-each-key="id" data-each-arg="item">
|
|
259
|
-
<li>{{item.label}}</li>
|
|
260
|
-
</ul>
|
|
261
|
-
</div>
|
|
262
|
-
`;
|
|
263
|
-
const root = container.querySelector('#root');
|
|
264
|
-
const list = root.querySelector('ul');
|
|
265
|
-
const handler = vi.fn();
|
|
266
|
-
list.addEventListener('haori:eachupdate', handler);
|
|
267
|
-
await Core.scan(root);
|
|
268
|
-
await Core.setBindingData(root, {
|
|
269
|
-
items: [
|
|
270
|
-
{ id: 'a', label: 'alpha' },
|
|
271
|
-
{ id: 'b', label: 'beta' },
|
|
272
|
-
{ id: 'c', label: 'gamma' },
|
|
273
|
-
],
|
|
274
|
-
});
|
|
275
|
-
await waitForDomSettled();
|
|
276
|
-
handler.mockClear();
|
|
277
|
-
await Core.setBindingData(root, {
|
|
278
|
-
items: [
|
|
279
|
-
{ id: 'b', label: 'beta' },
|
|
280
|
-
{ id: 'c', label: 'gamma' },
|
|
281
|
-
{ id: 'a', label: 'alpha' },
|
|
282
|
-
],
|
|
283
|
-
});
|
|
284
|
-
await waitForDomSettled();
|
|
285
|
-
expect(handler).toHaveBeenCalledTimes(1);
|
|
286
|
-
const event = handler.mock.calls[0][0];
|
|
287
|
-
expect(event.detail.added).toEqual([]);
|
|
288
|
-
expect(event.detail.removed).toEqual([]);
|
|
289
|
-
expect(event.detail.order).toEqual(['b', 'c', 'a']);
|
|
290
|
-
expect(event.detail.total).toBe(3);
|
|
291
|
-
});
|
|
292
|
-
test('data-each の追加削除同時更新でも eachupdate が差分を正しく通知する', async () => {
|
|
293
|
-
container.innerHTML = `
|
|
294
|
-
<div id="root">
|
|
295
|
-
<ul data-each="items" data-each-key="id" data-each-arg="item">
|
|
296
|
-
<li>{{item.label}}</li>
|
|
297
|
-
</ul>
|
|
298
|
-
</div>
|
|
299
|
-
`;
|
|
300
|
-
const root = container.querySelector('#root');
|
|
301
|
-
const list = root.querySelector('ul');
|
|
302
|
-
const handler = vi.fn();
|
|
303
|
-
list.addEventListener('haori:eachupdate', handler);
|
|
304
|
-
await Core.scan(root);
|
|
305
|
-
await Core.setBindingData(root, {
|
|
306
|
-
items: [
|
|
307
|
-
{ id: 'a', label: 'alpha' },
|
|
308
|
-
{ id: 'b', label: 'beta' },
|
|
309
|
-
{ id: 'c', label: 'gamma' },
|
|
310
|
-
],
|
|
311
|
-
});
|
|
312
|
-
await waitForDomSettled();
|
|
313
|
-
handler.mockClear();
|
|
314
|
-
await Core.setBindingData(root, {
|
|
315
|
-
items: [
|
|
316
|
-
{ id: 'b', label: 'beta' },
|
|
317
|
-
{ id: 'd', label: 'delta' },
|
|
318
|
-
],
|
|
319
|
-
});
|
|
320
|
-
await waitForDomSettled();
|
|
321
|
-
expect(handler).toHaveBeenCalledTimes(1);
|
|
322
|
-
const event = handler.mock.calls[0][0];
|
|
323
|
-
expect(event.detail.added).toEqual(['d']);
|
|
324
|
-
expect(event.detail.removed).toEqual(['a', 'c']);
|
|
325
|
-
expect(event.detail.order).toEqual(['b', 'd']);
|
|
326
|
-
expect(event.detail.total).toBe(2);
|
|
327
|
-
expect(Array.from(list.querySelectorAll('li')).map(item => item.textContent)).toEqual(['beta', 'delta']);
|
|
328
|
-
});
|
|
329
|
-
test('変更された再利用行は即時再評価のみ行い、遅延再評価を重ねない', async () => {
|
|
330
|
-
container.innerHTML = `
|
|
331
|
-
<div id="root">
|
|
332
|
-
<ul data-each="items" data-each-key="id" data-each-arg="item">
|
|
333
|
-
<li>{{renderSpy(item)}}</li>
|
|
334
|
-
</ul>
|
|
335
|
-
</div>
|
|
336
|
-
`;
|
|
337
|
-
const root = container.querySelector('#root');
|
|
338
|
-
const renderSpy = vi.fn((item) => item.label);
|
|
339
|
-
await Core.scan(root);
|
|
340
|
-
await Core.setBindingData(root, {
|
|
341
|
-
items: [
|
|
342
|
-
{ id: 'a', label: 'alpha' },
|
|
343
|
-
{ id: 'b', label: 'beta' },
|
|
344
|
-
],
|
|
345
|
-
renderSpy,
|
|
346
|
-
});
|
|
347
|
-
await waitForDomSettled();
|
|
348
|
-
await new Promise(resolve => setTimeout(resolve, 150));
|
|
349
|
-
await waitForDomSettled();
|
|
350
|
-
const baselineCalls = renderSpy.mock.calls.length;
|
|
351
|
-
await Core.setBindingData(root, {
|
|
352
|
-
items: [
|
|
353
|
-
{ id: 'a', label: 'alpha2' },
|
|
354
|
-
{ id: 'b', label: 'beta2' },
|
|
355
|
-
],
|
|
356
|
-
renderSpy,
|
|
357
|
-
});
|
|
358
|
-
await waitForDomSettled();
|
|
359
|
-
await new Promise(resolve => setTimeout(resolve, 150));
|
|
360
|
-
await waitForDomSettled();
|
|
361
|
-
expect(renderSpy).toHaveBeenCalledTimes(baselineCalls + 2);
|
|
362
|
-
expect(Array.from(container.querySelectorAll('li')).map(item => item.textContent)).toEqual(['alpha2', 'beta2']);
|
|
363
|
-
});
|
|
364
|
-
test('単純な新規 data-each 行では遅延再評価を追加しない', async () => {
|
|
365
|
-
container.innerHTML = `
|
|
366
|
-
<div id="root">
|
|
367
|
-
<ul data-each="items" data-each-key="id">
|
|
368
|
-
<li>{{renderSpy(label)}}</li>
|
|
369
|
-
</ul>
|
|
370
|
-
</div>
|
|
371
|
-
`;
|
|
372
|
-
const root = container.querySelector('#root');
|
|
373
|
-
const renderSpy = vi.fn((label) => label);
|
|
374
|
-
await Core.scan(root);
|
|
375
|
-
await Core.setBindingData(root, {
|
|
376
|
-
items: [
|
|
377
|
-
{ id: 'a', label: 'alpha' },
|
|
378
|
-
{ id: 'b', label: 'beta' },
|
|
379
|
-
],
|
|
380
|
-
renderSpy,
|
|
381
|
-
});
|
|
382
|
-
await waitForDomSettled();
|
|
383
|
-
await new Promise(resolve => setTimeout(resolve, 150));
|
|
384
|
-
await waitForDomSettled();
|
|
385
|
-
expect(renderSpy).toHaveBeenCalledTimes(2);
|
|
386
|
-
expect(Array.from(container.querySelectorAll('li')).map(item => item.textContent)).toEqual(['alpha', 'beta']);
|
|
387
|
-
});
|
|
388
|
-
test('fresh clone 初期化では静的 subtree の属性初期化をスキップする', async () => {
|
|
389
|
-
container.innerHTML = `
|
|
390
|
-
<div id="root">
|
|
391
|
-
<ul data-each="items" data-each-key="id" data-each-arg="item">
|
|
392
|
-
<li>
|
|
393
|
-
<section class="static-block">
|
|
394
|
-
<div class="static-inner">
|
|
395
|
-
<span>固定ラベル</span>
|
|
396
|
-
</div>
|
|
397
|
-
</section>
|
|
398
|
-
<strong>{{item.label}}</strong>
|
|
399
|
-
</li>
|
|
400
|
-
</ul>
|
|
401
|
-
</div>
|
|
402
|
-
`;
|
|
403
|
-
const root = container.querySelector('#root');
|
|
404
|
-
const list = root.querySelector('ul');
|
|
405
|
-
await Core.scan(root);
|
|
406
|
-
const initializeElementAttributesSpy = vi.spyOn(Core, 'initializeElementAttributes');
|
|
407
|
-
await Core.setBindingData(root, {
|
|
408
|
-
items: [
|
|
409
|
-
{ id: 'a', label: 'alpha' },
|
|
410
|
-
{ id: 'b', label: 'beta' },
|
|
411
|
-
],
|
|
412
|
-
});
|
|
413
|
-
await waitForDomSettled();
|
|
414
|
-
expect(Array.from(list.querySelectorAll('strong')).map(item => item.textContent)).toEqual(['alpha', 'beta']);
|
|
415
|
-
expect(list.querySelectorAll('.static-block')).toHaveLength(2);
|
|
416
|
-
expect(Array.from(list.querySelectorAll('.static-block')).map(item => item.textContent?.trim())).toEqual(['固定ラベル', '固定ラベル']);
|
|
417
|
-
expect(initializeElementAttributesSpy.mock.calls.filter(([fragment]) => fragment.getTarget().classList.contains('static-block'))).toHaveLength(0);
|
|
418
|
-
initializeElementAttributesSpy.mockRestore();
|
|
419
|
-
});
|
|
420
|
-
test('data-each 配下の data-if が行ごとに評価され、ページネーション相当の表示が崩れない', async () => {
|
|
421
|
-
container.innerHTML = `
|
|
422
|
-
<div
|
|
423
|
-
data-bind='{"currentPage":1,"pages":[
|
|
424
|
-
{"p":0,"ellipsis":true},
|
|
425
|
-
{"p":1,"ellipsis":false},
|
|
426
|
-
{"p":2,"ellipsis":false}
|
|
427
|
-
]}'
|
|
428
|
-
>
|
|
429
|
-
<ul data-each="pages" data-each-key="p">
|
|
430
|
-
<li class="{{ellipsis ? 'disabled' : p === currentPage ? 'active' : ''}}">
|
|
431
|
-
<span data-if="ellipsis" class="page-link" aria-hidden="true">…</span>
|
|
432
|
-
<span data-if="!ellipsis && p === currentPage" class="page-link" aria-current="page">{{p + 1}}</span>
|
|
433
|
-
<button data-if="!ellipsis && p !== currentPage" type="button" class="page-link">{{p + 1}}</button>
|
|
434
|
-
</li>
|
|
435
|
-
</ul>
|
|
436
|
-
</div>
|
|
437
|
-
`;
|
|
438
|
-
const root = container.querySelector('div');
|
|
439
|
-
const list = root.querySelector('ul');
|
|
440
|
-
await Core.scan(root);
|
|
441
|
-
await waitForDomSettled();
|
|
442
|
-
const items = Array.from(list.querySelectorAll('li'));
|
|
443
|
-
expect(items).toHaveLength(3);
|
|
444
|
-
const visibleChildren = items.map(item => Array.from(item.children).filter(child => !child.hasAttribute('data-if-false')));
|
|
445
|
-
expect(visibleChildren.map(children => children.length)).toEqual([1, 1, 1]);
|
|
446
|
-
expect(visibleChildren[0][0].textContent).toBe('…');
|
|
447
|
-
expect(visibleChildren[1][0].textContent).toBe('2');
|
|
448
|
-
expect(visibleChildren[2][0].textContent).toBe('3');
|
|
449
|
-
expect(items[0].classList.contains('disabled')).toBe(true);
|
|
450
|
-
expect(items[1].classList.contains('active')).toBe(true);
|
|
451
|
-
expect(items[2].classList.contains('active')).toBe(false);
|
|
452
|
-
});
|
|
453
|
-
test('plain nested data-each の入力が同値なら子 data-each の再評価をスキップする', async () => {
|
|
454
|
-
container.innerHTML = `
|
|
455
|
-
<div
|
|
456
|
-
id="root"
|
|
457
|
-
data-bind='{
|
|
458
|
-
"plans":[
|
|
459
|
-
{
|
|
460
|
-
"id":"basic",
|
|
461
|
-
"name":"基本",
|
|
462
|
-
"options":[{"id":"mail","label":"メール"},{"id":"chat","label":"チャット"}]
|
|
463
|
-
},
|
|
464
|
-
{
|
|
465
|
-
"id":"plus",
|
|
466
|
-
"name":"拡張",
|
|
467
|
-
"options":[{"id":"phone","label":"電話"}]
|
|
468
|
-
}
|
|
469
|
-
]
|
|
470
|
-
}'
|
|
471
|
-
>
|
|
472
|
-
<ul id="plans" data-each="plans" data-each-arg="plan" data-each-key="id">
|
|
473
|
-
<li>
|
|
474
|
-
<span class="plan-name">{{plan.name}}</span>
|
|
475
|
-
<ul class="option-list" data-each="plan.options" data-each-arg="option" data-each-key="id">
|
|
476
|
-
<li>{{option.label}}</li>
|
|
477
|
-
</ul>
|
|
478
|
-
</li>
|
|
479
|
-
</ul>
|
|
480
|
-
</div>
|
|
481
|
-
`;
|
|
482
|
-
const root = container.querySelector('#root');
|
|
483
|
-
await Core.scan(root);
|
|
484
|
-
await waitForDomSettled();
|
|
485
|
-
const evaluateEachSpy = vi.spyOn(Core, 'evaluateEach');
|
|
486
|
-
await Core.setBindingData(root, {
|
|
487
|
-
plans: [
|
|
488
|
-
{
|
|
489
|
-
id: 'basic',
|
|
490
|
-
name: '基本改',
|
|
491
|
-
options: [
|
|
492
|
-
{ id: 'mail', label: 'メール' },
|
|
493
|
-
{ id: 'chat', label: 'チャット' },
|
|
494
|
-
],
|
|
495
|
-
},
|
|
496
|
-
{
|
|
497
|
-
id: 'plus',
|
|
498
|
-
name: '拡張改',
|
|
499
|
-
options: [
|
|
500
|
-
{ id: 'phone', label: '電話' },
|
|
501
|
-
],
|
|
502
|
-
},
|
|
503
|
-
],
|
|
504
|
-
});
|
|
505
|
-
await waitForDomSettled();
|
|
506
|
-
expect(Array.from(container.querySelectorAll('.plan-name')).map(item => item.textContent?.trim())).toEqual(['基本改', '拡張改']);
|
|
507
|
-
expect(Array.from(container.querySelectorAll('.option-list > li')).map(item => item.textContent?.trim())).toEqual(['メール', 'チャット', '電話']);
|
|
508
|
-
expect(evaluateEachSpy.mock.calls.filter(([fragment]) => fragment.getTarget().classList.contains('option-list'))).toHaveLength(0);
|
|
509
|
-
evaluateEachSpy.mockRestore();
|
|
510
|
-
});
|
|
511
|
-
test('data-if が false の行では子孫式を評価せず console.error を出さない', async () => {
|
|
512
|
-
const consoleSpy = vi
|
|
513
|
-
.spyOn(console, 'error')
|
|
514
|
-
.mockImplementation(() => { });
|
|
515
|
-
container.innerHTML = `
|
|
516
|
-
<div
|
|
517
|
-
data-bind='{"items":[{"id":1,"secondary":null},{"id":2,"secondary":{"name":"表示対象"}}]}'
|
|
518
|
-
>
|
|
519
|
-
<ul data-each="items" data-each-key="id" data-each-arg="item">
|
|
520
|
-
<li>
|
|
521
|
-
<div data-if="item.secondary">
|
|
522
|
-
<span class="secondary-name">{{item.secondary.name}}</span>
|
|
523
|
-
</div>
|
|
524
|
-
</li>
|
|
525
|
-
</ul>
|
|
526
|
-
</div>
|
|
527
|
-
`;
|
|
528
|
-
const root = container.querySelector('div');
|
|
529
|
-
const list = root.querySelector('ul');
|
|
530
|
-
await Core.scan(root);
|
|
531
|
-
await waitForDomSettled();
|
|
532
|
-
const items = Array.from(list.querySelectorAll('li'));
|
|
533
|
-
expect(items).toHaveLength(2);
|
|
534
|
-
const firstConditional = items[0].querySelector('div');
|
|
535
|
-
const secondConditional = items[1].querySelector('div');
|
|
536
|
-
expect(firstConditional.hasAttribute('data-if-false')).toBe(true);
|
|
537
|
-
expect(secondConditional.hasAttribute('data-if-false')).toBe(false);
|
|
538
|
-
expect(items[1].querySelector('.secondary-name')?.textContent).toBe('表示対象');
|
|
539
|
-
expect(consoleSpy).not.toHaveBeenCalled();
|
|
540
|
-
});
|
|
541
|
-
test('初回に false の data-if 配下でも bind 更新後に data-each が初期化される', async () => {
|
|
542
|
-
container.innerHTML = `
|
|
543
|
-
<div data-bind='{"record":{"totalCount":null,"items":[]}}'>
|
|
544
|
-
<section data-if="record.totalCount != null">
|
|
545
|
-
<ul data-each="record.items" data-each-key="id" data-each-arg="item">
|
|
546
|
-
<li class="item">{{item.name}}</li>
|
|
547
|
-
</ul>
|
|
548
|
-
</section>
|
|
549
|
-
</div>
|
|
550
|
-
`;
|
|
551
|
-
const root = container.querySelector('div');
|
|
552
|
-
const section = root.querySelector('section');
|
|
553
|
-
const list = root.querySelector('ul');
|
|
554
|
-
await Core.scan(root);
|
|
555
|
-
await waitForDomSettled();
|
|
556
|
-
expect(section.hasAttribute('data-if-false')).toBe(true);
|
|
557
|
-
expect(list.querySelectorAll('li')).toHaveLength(1);
|
|
558
|
-
expect(list.textContent).toContain('{{item.name}}');
|
|
559
|
-
await Core.setBindingData(root, {
|
|
560
|
-
record: {
|
|
561
|
-
totalCount: 2,
|
|
562
|
-
items: [
|
|
563
|
-
{ id: 1, name: 'A' },
|
|
564
|
-
{ id: 2, name: 'B' },
|
|
565
|
-
],
|
|
566
|
-
},
|
|
567
|
-
});
|
|
568
|
-
await waitForDomSettled();
|
|
569
|
-
const items = Array.from(list.querySelectorAll('li'));
|
|
570
|
-
expect(section.hasAttribute('data-if-false')).toBe(false);
|
|
571
|
-
expect(items).toHaveLength(2);
|
|
572
|
-
expect(items.map(item => item.textContent?.trim())).toEqual(['A', 'B']);
|
|
573
|
-
expect(list.textContent).not.toContain('{{item.name}}');
|
|
574
|
-
});
|
|
575
|
-
test('表示済みの data-if 配下は true から false を経て true に戻ると evaluateAll で再評価される', async () => {
|
|
576
|
-
container.innerHTML = `
|
|
577
|
-
<div data-bind='{"visible":true,"items":[{"id":1,"name":"A"}]}'>
|
|
578
|
-
<section data-if="visible">
|
|
579
|
-
<ul data-each="items" data-each-key="id" data-each-arg="item">
|
|
580
|
-
<li class="item">{{item.name}}</li>
|
|
581
|
-
</ul>
|
|
582
|
-
</section>
|
|
583
|
-
</div>
|
|
584
|
-
`;
|
|
585
|
-
const root = container.querySelector('div');
|
|
586
|
-
const section = root.querySelector('section');
|
|
587
|
-
const list = root.querySelector('ul');
|
|
588
|
-
await Core.scan(root);
|
|
589
|
-
await waitForDomSettled();
|
|
590
|
-
const listFragment = Fragment.get(list);
|
|
591
|
-
expect(listFragment.isMounted()).toBe(true);
|
|
592
|
-
expect(Array.from(list.querySelectorAll('li')).map(item => item.textContent?.trim())).toEqual(['A']);
|
|
593
|
-
await Core.setBindingData(root, {
|
|
594
|
-
visible: false,
|
|
595
|
-
items: [{ id: 1, name: 'A' }],
|
|
596
|
-
});
|
|
597
|
-
await waitForDomSettled();
|
|
598
|
-
expect(section.hasAttribute('data-if-false')).toBe(true);
|
|
599
|
-
const evaluateAllSpy = vi.spyOn(Core, 'evaluateAll');
|
|
600
|
-
const scanSpy = vi.spyOn(Core, 'scan');
|
|
601
|
-
await Core.setBindingData(root, {
|
|
602
|
-
visible: true,
|
|
603
|
-
items: [
|
|
604
|
-
{ id: 1, name: 'A2' },
|
|
605
|
-
{ id: 2, name: 'B' },
|
|
606
|
-
],
|
|
607
|
-
});
|
|
608
|
-
await waitForDomSettled();
|
|
609
|
-
expect(section.hasAttribute('data-if-false')).toBe(false);
|
|
610
|
-
expect(Array.from(list.querySelectorAll('li')).map(item => item.textContent?.trim())).toEqual(['A2', 'B']);
|
|
611
|
-
expect(evaluateAllSpy.mock.calls.some(call => call[0] === listFragment)).toBe(true);
|
|
612
|
-
expect(scanSpy.mock.calls.some(call => call[0] === list)).toBe(false);
|
|
613
|
-
evaluateAllSpy.mockRestore();
|
|
614
|
-
scanSpy.mockRestore();
|
|
615
|
-
});
|
|
616
|
-
test('data-each 配下の a タグに data-if と href プレースホルダが共存する場合に正しく hide/show される', async () => {
|
|
617
|
-
container.innerHTML = `
|
|
618
|
-
<table>
|
|
619
|
-
<tbody
|
|
620
|
-
data-bind='{"content":[
|
|
621
|
-
{"id":1,"customerCode":"C001","category":"顧客","billingId":null},
|
|
622
|
-
{"id":2,"customerCode":"C002","category":"請求","billingId":"B001"},
|
|
623
|
-
{"id":3,"customerCode":"C003","category":"入金","billingId":null}
|
|
624
|
-
]}'
|
|
625
|
-
data-each="content"
|
|
626
|
-
data-each-key="id"
|
|
627
|
-
>
|
|
628
|
-
<tr>
|
|
629
|
-
<td>{{customerCode}}</td>
|
|
630
|
-
<td>{{category}}</td>
|
|
631
|
-
<td>
|
|
632
|
-
<a data-if="category === '顧客'" href="customer-list.html?customerCode={{customerCode}}">顧客対応</a>
|
|
633
|
-
<a
|
|
634
|
-
data-if="category === '請求'"
|
|
635
|
-
href="billing-list.html?customerCode={{customerCode}}&billingId={{billingId}}"
|
|
636
|
-
>請求対応</a>
|
|
637
|
-
<a data-if="category === '入金'" href="payment-list.html?customerCode={{customerCode}}">入金対応</a>
|
|
638
|
-
</td>
|
|
639
|
-
</tr>
|
|
640
|
-
</tbody>
|
|
641
|
-
</table>
|
|
642
|
-
`;
|
|
643
|
-
const tbody = container.querySelector('tbody');
|
|
644
|
-
await Core.scan(tbody);
|
|
645
|
-
await waitForCondition(() => tbody.querySelectorAll('tr').length === 3, { description: 'tbody rows' });
|
|
646
|
-
await waitForDomSettled();
|
|
647
|
-
expect(tbody.querySelectorAll('[data-if-false]').length).toBeGreaterThan(0);
|
|
648
|
-
const rows = Array.from(tbody.querySelectorAll('tr'));
|
|
649
|
-
expect(rows).toHaveLength(3);
|
|
650
|
-
const row0Links = Array.from(rows[0].querySelectorAll('a'));
|
|
651
|
-
const row1Links = Array.from(rows[1].querySelectorAll('a'));
|
|
652
|
-
const row2Links = Array.from(rows[2].querySelectorAll('a'));
|
|
653
|
-
// row0: category=顧客 → 顧客リンク表示、請求・入金リンク非表示
|
|
654
|
-
expect(row0Links[0].hasAttribute('data-if-false')).toBe(false);
|
|
655
|
-
expect(row0Links[1].hasAttribute('data-if-false')).toBe(true);
|
|
656
|
-
expect(row0Links[2].hasAttribute('data-if-false')).toBe(true);
|
|
657
|
-
// href プレースホルダが行データで展開されていること
|
|
658
|
-
expect(row0Links[0].getAttribute('href')).toBe('customer-list.html?customerCode=C001');
|
|
659
|
-
// row1: category=請求 → 顧客リンク非表示、請求リンク表示、入金リンク非表示
|
|
660
|
-
expect(row1Links[0].hasAttribute('data-if-false')).toBe(true);
|
|
661
|
-
expect(row1Links[1].hasAttribute('data-if-false')).toBe(false);
|
|
662
|
-
expect(row1Links[2].hasAttribute('data-if-false')).toBe(true);
|
|
663
|
-
expect(row1Links[1].getAttribute('href')).toBe('billing-list.html?customerCode=C002&billingId=B001');
|
|
664
|
-
// row2: category=入金 → 顧客・請求リンク非表示、入金リンク表示
|
|
665
|
-
expect(row2Links[0].hasAttribute('data-if-false')).toBe(true);
|
|
666
|
-
expect(row2Links[1].hasAttribute('data-if-false')).toBe(true);
|
|
667
|
-
expect(row2Links[2].hasAttribute('data-if-false')).toBe(false);
|
|
668
|
-
expect(row2Links[2].getAttribute('href')).toBe('payment-list.html?customerCode=C003');
|
|
669
|
-
});
|
|
670
|
-
test('data-attr-src が生値を維持したまま実属性を再評価する', async () => {
|
|
671
|
-
container.innerHTML = `
|
|
672
|
-
<img
|
|
673
|
-
data-bind='{"id":"before"}'
|
|
674
|
-
data-attr-src="img/{{id}}.jpg"
|
|
675
|
-
alt="preview"
|
|
676
|
-
>
|
|
677
|
-
`;
|
|
678
|
-
const image = container.querySelector('img');
|
|
679
|
-
await Core.scan(image);
|
|
680
|
-
await waitForDomSettled();
|
|
681
|
-
// data-attr-* の生値は保持したまま、実属性へ評価結果を反映すること。
|
|
682
|
-
expect(image.getAttribute('data-attr-src')).toBe('img/{{id}}.jpg');
|
|
683
|
-
expect(image.getAttribute('src')).toBe('img/before.jpg');
|
|
684
|
-
await Core.setBindingData(image, { id: 'after' });
|
|
685
|
-
await waitForDomSettled();
|
|
686
|
-
// バインディング変更時も生値を維持したまま実属性だけ更新されること。
|
|
687
|
-
expect(image.getAttribute('data-attr-src')).toBe('img/{{id}}.jpg');
|
|
688
|
-
expect(image.getAttribute('src')).toBe('img/after.jpg');
|
|
689
|
-
});
|
|
690
|
-
test('MutationObserver経由の書き戻しでdata-attr-*のattributeMapが上書きされない', async () => {
|
|
691
|
-
container.innerHTML = `
|
|
692
|
-
<img
|
|
693
|
-
data-bind='{"id":"before"}'
|
|
694
|
-
data-attr-src="img/{{id}}.jpg"
|
|
695
|
-
alt="preview"
|
|
696
|
-
>
|
|
697
|
-
`;
|
|
698
|
-
const image = container.querySelector('img');
|
|
699
|
-
await Core.scan(image);
|
|
700
|
-
await waitForDomSettled();
|
|
701
|
-
expect(image.getAttribute('src')).toBe('img/before.jpg');
|
|
702
|
-
// Observer経由の書き戻しをシミュレート: 展開済みの値でsetAttributeを呼ぶ
|
|
703
|
-
const fragment = Fragment.get(image);
|
|
704
|
-
await fragment.setAliasedAttribute('data-attr-src', 'src', 'img/before.jpg', true);
|
|
705
|
-
await waitForDomSettled();
|
|
706
|
-
// attributeMapのテンプレート式が保持され、再バインドで正しく展開されること。
|
|
707
|
-
await Core.setBindingData(image, { id: 'after' });
|
|
708
|
-
await waitForDomSettled();
|
|
709
|
-
expect(image.getAttribute('src')).toBe('img/after.jpg');
|
|
710
|
-
});
|
|
711
|
-
test('data-attr-value は value 属性だけを更新し現在値は上書きしない', async () => {
|
|
712
|
-
container.innerHTML = `
|
|
713
|
-
<div data-bind='{"count":"1"}'>
|
|
714
|
-
<input type="text" data-attr-value="{{count}}">
|
|
715
|
-
</div>
|
|
716
|
-
`;
|
|
717
|
-
const root = container.querySelector('div');
|
|
718
|
-
const input = root.querySelector('input');
|
|
719
|
-
await Core.scan(root);
|
|
720
|
-
await waitForDomSettled();
|
|
721
|
-
// 初回評価では value 属性へ反映されること。
|
|
722
|
-
expect(input.getAttribute('value')).toBe('1');
|
|
723
|
-
input.value = 'manual';
|
|
724
|
-
await Core.setBindingData(root, { count: '2' });
|
|
725
|
-
await waitForDomSettled();
|
|
726
|
-
// 再評価では value 属性だけ更新し、現在値 property は維持すること。
|
|
727
|
-
expect(input.getAttribute('value')).toBe('2');
|
|
728
|
-
expect(input.value).toBe('manual');
|
|
729
|
-
});
|
|
730
|
-
test('setBindingData で false になった通常属性は削除される', async () => {
|
|
731
|
-
container.innerHTML = `
|
|
732
|
-
<div data-bind='{"enabled":true}'>
|
|
733
|
-
<span class="{{enabled && 'active'}}">label</span>
|
|
734
|
-
</div>
|
|
735
|
-
`;
|
|
736
|
-
const root = container.querySelector('div');
|
|
737
|
-
const target = root.querySelector('span');
|
|
738
|
-
await Core.scan(root);
|
|
739
|
-
// 初回評価では属性が付与されること。
|
|
740
|
-
expect(target.getAttribute('class')).toBe('active');
|
|
741
|
-
await Core.setBindingData(root, { enabled: false });
|
|
742
|
-
// false 評価では通常属性が DOM から削除されること。
|
|
743
|
-
expect(target.hasAttribute('class')).toBe(false);
|
|
744
|
-
});
|
|
745
|
-
test('data-each の再利用行でも null になった通常属性は削除される', async () => {
|
|
746
|
-
container.innerHTML = `
|
|
747
|
-
<div
|
|
748
|
-
data-bind='{"items":[{"label":"A"},{"label":"B"}]}'
|
|
749
|
-
>
|
|
750
|
-
<ul data-each="items" data-each-arg="item">
|
|
751
|
-
<li title="{{item.label}}">{{item.label ?? '-'}}</li>
|
|
752
|
-
</ul>
|
|
753
|
-
</div>
|
|
754
|
-
`;
|
|
755
|
-
const root = container.querySelector('div');
|
|
756
|
-
const list = root.querySelector('ul');
|
|
757
|
-
await Core.scan(root);
|
|
758
|
-
// 初回の行生成では通常属性が各行に反映されること。
|
|
759
|
-
expect(Array.from(list.querySelectorAll('li')).map(item => item.getAttribute('title'))).toEqual(['A', 'B']);
|
|
760
|
-
await Core.setBindingData(root, {
|
|
761
|
-
items: [{ label: null }, { label: 'B2' }],
|
|
762
|
-
});
|
|
763
|
-
const items = Array.from(list.querySelectorAll('li'));
|
|
764
|
-
// 再利用行でも null 評価の属性だけが削除されること。
|
|
765
|
-
expect(items[0].hasAttribute('title')).toBe(false);
|
|
766
|
-
expect(items[1].getAttribute('title')).toBe('B2');
|
|
767
|
-
});
|
|
768
|
-
});
|
|
769
|
-
describe('data-import: data-importing 属性', () => {
|
|
770
|
-
beforeEach(() => {
|
|
771
|
-
vi.resetAllMocks();
|
|
772
|
-
});
|
|
773
|
-
afterEach(() => {
|
|
774
|
-
vi.restoreAllMocks();
|
|
775
|
-
});
|
|
776
|
-
it('インポート中は data-importing 属性が付与される', async () => {
|
|
777
|
-
let resolveFetch;
|
|
778
|
-
const deferredFetch = new Promise(resolve => (resolveFetch = resolve));
|
|
779
|
-
vi.spyOn(globalThis, 'fetch').mockReturnValue(deferredFetch);
|
|
780
|
-
const el = document.createElement('div');
|
|
781
|
-
el.setAttribute('data-import', '/header.html');
|
|
782
|
-
container.appendChild(el);
|
|
783
|
-
const scanPromise = Core.scan(el);
|
|
784
|
-
await waitForCondition(() => el.hasAttribute('data-importing'), {
|
|
785
|
-
description: 'data-importing が付与されること',
|
|
786
|
-
});
|
|
787
|
-
expect(el.hasAttribute('data-importing')).toBe(true);
|
|
788
|
-
resolveFetch({
|
|
789
|
-
ok: true,
|
|
790
|
-
text: async () => '<html><body><nav>Header</nav></body></html>',
|
|
791
|
-
});
|
|
792
|
-
await scanPromise;
|
|
793
|
-
await waitForDomSettled();
|
|
794
|
-
expect(el.hasAttribute('data-importing')).toBe(false);
|
|
795
|
-
});
|
|
796
|
-
it('インポート完了後は data-importing 属性が除去される', async () => {
|
|
797
|
-
vi.spyOn(globalThis, 'fetch').mockResolvedValue({
|
|
798
|
-
ok: true,
|
|
799
|
-
text: async () => '<html><body><nav>Header</nav></body></html>',
|
|
800
|
-
});
|
|
801
|
-
const el = document.createElement('div');
|
|
802
|
-
el.setAttribute('data-import', '/header.html');
|
|
803
|
-
container.appendChild(el);
|
|
804
|
-
await Core.scan(el);
|
|
805
|
-
await waitForDomSettled();
|
|
806
|
-
expect(el.hasAttribute('data-importing')).toBe(false);
|
|
807
|
-
expect(el.innerHTML).toContain('Header');
|
|
808
|
-
});
|
|
809
|
-
it('インポート失敗時も data-importing 属性が除去される', async () => {
|
|
810
|
-
vi.spyOn(globalThis, 'fetch').mockResolvedValue({
|
|
811
|
-
ok: false,
|
|
812
|
-
status: 500,
|
|
813
|
-
statusText: 'Internal Server Error',
|
|
814
|
-
});
|
|
815
|
-
const el = document.createElement('div');
|
|
816
|
-
el.setAttribute('data-import', '/header.html');
|
|
817
|
-
container.appendChild(el);
|
|
818
|
-
await Core.scan(el);
|
|
819
|
-
await waitForDomSettled();
|
|
820
|
-
expect(el.hasAttribute('data-importing')).toBe(false);
|
|
821
|
-
});
|
|
822
|
-
it('未解決参照は初回インポートを停止し、bind 更新で解決したら URL 変更時のみ再実行する', async () => {
|
|
823
|
-
const fetchSpy = vi
|
|
824
|
-
.spyOn(globalThis, 'fetch')
|
|
825
|
-
.mockImplementation((input) => {
|
|
826
|
-
const url = String(input);
|
|
827
|
-
const body = url.includes('footer')
|
|
828
|
-
? '<html><body><footer>Footer</footer></body></html>'
|
|
829
|
-
: '<html><body><nav>Header</nav></body></html>';
|
|
830
|
-
return Promise.resolve({
|
|
831
|
-
ok: true,
|
|
832
|
-
text: async () => body,
|
|
833
|
-
});
|
|
834
|
-
});
|
|
835
|
-
const root = document.createElement('div');
|
|
836
|
-
const el = document.createElement('div');
|
|
837
|
-
el.setAttribute('data-import', '/partials/{{view}}.html');
|
|
838
|
-
root.appendChild(el);
|
|
839
|
-
container.appendChild(root);
|
|
840
|
-
await Core.scan(root);
|
|
841
|
-
await waitForDomSettled();
|
|
842
|
-
expect(fetchSpy).not.toHaveBeenCalled();
|
|
843
|
-
await Core.setBindingData(root, { view: 'header' });
|
|
844
|
-
await waitForCondition(() => fetchSpy.mock.calls.length === 1, {
|
|
845
|
-
description: 'header import after bind update',
|
|
846
|
-
});
|
|
847
|
-
expect(fetchSpy.mock.calls[0][0]).toBe('/partials/header.html');
|
|
848
|
-
expect(el.innerHTML).toContain('Header');
|
|
849
|
-
await Core.setBindingData(root, { view: 'header' });
|
|
850
|
-
await waitForDomSettled();
|
|
851
|
-
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
852
|
-
await Core.setBindingData(root, { view: 'footer' });
|
|
853
|
-
await waitForCondition(() => fetchSpy.mock.calls.length === 2, {
|
|
854
|
-
description: 'footer import after url change',
|
|
855
|
-
});
|
|
856
|
-
expect(fetchSpy.mock.calls[1][0]).toBe('/partials/footer.html');
|
|
857
|
-
expect(el.innerHTML).toContain('Footer');
|
|
858
|
-
});
|
|
859
|
-
it('初回 false の data-if 配下にある data-import は表示後に初期化される', async () => {
|
|
860
|
-
const fetchSpy = vi
|
|
861
|
-
.spyOn(globalThis, 'fetch')
|
|
862
|
-
.mockImplementation((input) => {
|
|
863
|
-
const url = String(input);
|
|
864
|
-
return Promise.resolve({
|
|
865
|
-
ok: true,
|
|
866
|
-
text: async () => `<html><body><nav>${url}</nav></body></html>`,
|
|
867
|
-
});
|
|
868
|
-
});
|
|
869
|
-
container.innerHTML = `
|
|
870
|
-
<div data-bind='{"visible":false,"view":"header"}'>
|
|
871
|
-
<section data-if="visible">
|
|
872
|
-
<div id="import-target" data-import="/partials/{{view}}.html"></div>
|
|
873
|
-
</section>
|
|
874
|
-
</div>
|
|
875
|
-
`;
|
|
876
|
-
const root = container.querySelector('div');
|
|
877
|
-
const section = root.querySelector('section');
|
|
878
|
-
const target = root.querySelector('#import-target');
|
|
879
|
-
await Core.scan(root);
|
|
880
|
-
await waitForDomSettled();
|
|
881
|
-
expect(section.hasAttribute('data-if-false')).toBe(true);
|
|
882
|
-
expect(fetchSpy).not.toHaveBeenCalled();
|
|
883
|
-
await Core.setBindingData(root, { visible: true, view: 'header' });
|
|
884
|
-
await waitForCondition(() => fetchSpy.mock.calls.length === 1, {
|
|
885
|
-
description: 'import after false data-if becomes visible',
|
|
886
|
-
});
|
|
887
|
-
expect(section.hasAttribute('data-if-false')).toBe(false);
|
|
888
|
-
expect(fetchSpy.mock.calls[0][0]).toBe('/partials/header.html');
|
|
889
|
-
expect(target.innerHTML).toContain('/partials/header.html');
|
|
890
|
-
});
|
|
891
|
-
});
|
|
892
|
-
describe('bind 更新時の data-fetch 再評価', () => {
|
|
893
|
-
beforeEach(() => {
|
|
894
|
-
vi.resetAllMocks();
|
|
895
|
-
});
|
|
896
|
-
afterEach(() => {
|
|
897
|
-
vi.restoreAllMocks();
|
|
898
|
-
});
|
|
899
|
-
it('未解決参照は初回フェッチを停止し、シグネチャが変わったときだけ再実行する', async () => {
|
|
900
|
-
const fetchSpy = vi
|
|
901
|
-
.spyOn(globalThis, 'fetch')
|
|
902
|
-
.mockImplementation((_input, init) => {
|
|
903
|
-
const request = init?.body ? JSON.parse(String(init.body)) : {};
|
|
904
|
-
return Promise.resolve(new Response(JSON.stringify({ query: request.query }), {
|
|
905
|
-
headers: { 'Content-Type': 'application/json' },
|
|
906
|
-
}));
|
|
907
|
-
});
|
|
908
|
-
const root = document.createElement('div');
|
|
909
|
-
const el = document.createElement('div');
|
|
910
|
-
const bindTarget = document.createElement('div');
|
|
911
|
-
bindTarget.id = 'fetch-result';
|
|
912
|
-
el.setAttribute('data-fetch', 'http://api.test/search');
|
|
913
|
-
el.setAttribute('data-fetch-method', 'POST');
|
|
914
|
-
el.setAttribute('data-fetch-data', 'query={{query}}');
|
|
915
|
-
el.setAttribute('data-fetch-bind', '#fetch-result');
|
|
916
|
-
root.appendChild(el);
|
|
917
|
-
root.appendChild(bindTarget);
|
|
918
|
-
container.appendChild(root);
|
|
919
|
-
await Core.scan(root);
|
|
920
|
-
await waitForDomSettled();
|
|
921
|
-
expect(fetchSpy).not.toHaveBeenCalled();
|
|
922
|
-
await Core.setBindingData(root, { query: 'alpha' });
|
|
923
|
-
await waitForCondition(() => fetchSpy.mock.calls.length === 1, {
|
|
924
|
-
description: 'first fetch after query resolved',
|
|
925
|
-
});
|
|
926
|
-
expect(fetchSpy.mock.calls[0][0]).toBe('http://api.test/search');
|
|
927
|
-
expect(fetchSpy.mock.calls[0][1]?.body).toBe(JSON.stringify({ query: 'alpha' }));
|
|
928
|
-
await Core.setBindingData(root, { query: 'alpha' });
|
|
929
|
-
await waitForDomSettled();
|
|
930
|
-
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
931
|
-
await Core.setBindingData(root, { query: 'beta' });
|
|
932
|
-
await waitForCondition(() => fetchSpy.mock.calls.length === 2, {
|
|
933
|
-
description: 'second fetch after signature change',
|
|
934
|
-
});
|
|
935
|
-
expect(fetchSpy.mock.calls[1][1]?.body).toBe(JSON.stringify({ query: 'beta' }));
|
|
936
|
-
});
|
|
937
|
-
it('フェッチ結果の bind 更新が同一シグネチャなら再フェッチを起こさない', async () => {
|
|
938
|
-
const fetchSpy = vi
|
|
939
|
-
.spyOn(globalThis, 'fetch')
|
|
940
|
-
.mockResolvedValue(new Response(JSON.stringify({ query: 'alpha' }), {
|
|
941
|
-
headers: { 'Content-Type': 'application/json' },
|
|
942
|
-
}));
|
|
943
|
-
const root = document.createElement('div');
|
|
944
|
-
root.setAttribute('data-bind', '{"query":"alpha"}');
|
|
945
|
-
const el = document.createElement('div');
|
|
946
|
-
el.setAttribute('data-fetch', 'http://api.test/search?query={{query}}');
|
|
947
|
-
root.appendChild(el);
|
|
948
|
-
container.appendChild(root);
|
|
949
|
-
await Core.scan(root);
|
|
950
|
-
await waitForCondition(() => fetchSpy.mock.calls.length === 1, {
|
|
951
|
-
description: 'initial fetch',
|
|
952
|
-
});
|
|
953
|
-
await waitForDomSettled();
|
|
954
|
-
expect(fetchSpy).toHaveBeenCalledTimes(1);
|
|
955
|
-
expect(fetchSpy.mock.calls[0][0]).toBe('http://api.test/search?query=alpha');
|
|
956
|
-
});
|
|
957
|
-
it('初回 false の data-if 配下にある data-fetch は表示後に初期化される', async () => {
|
|
958
|
-
const fetchSpy = vi
|
|
959
|
-
.spyOn(globalThis, 'fetch')
|
|
960
|
-
.mockResolvedValue(new Response(JSON.stringify({ query: 'alpha' }), {
|
|
961
|
-
headers: { 'Content-Type': 'application/json' },
|
|
962
|
-
}));
|
|
963
|
-
container.innerHTML = `
|
|
964
|
-
<div data-bind='{"visible":false,"query":"alpha"}'>
|
|
965
|
-
<section data-if="visible">
|
|
966
|
-
<div
|
|
967
|
-
data-fetch="http://api.test/search?query={{query}}"
|
|
968
|
-
data-fetch-bind="#fetch-result"></div>
|
|
969
|
-
</section>
|
|
970
|
-
<div id="fetch-result"></div>
|
|
971
|
-
</div>
|
|
972
|
-
`;
|
|
973
|
-
const root = container.querySelector('div');
|
|
974
|
-
const section = root.querySelector('section');
|
|
975
|
-
const bindTarget = root.querySelector('#fetch-result');
|
|
976
|
-
await Core.scan(root);
|
|
977
|
-
await waitForDomSettled();
|
|
978
|
-
expect(section.hasAttribute('data-if-false')).toBe(true);
|
|
979
|
-
expect(fetchSpy).not.toHaveBeenCalled();
|
|
980
|
-
await Core.setBindingData(root, { visible: true, query: 'alpha' });
|
|
981
|
-
await waitForCondition(() => fetchSpy.mock.calls.length === 1, {
|
|
982
|
-
description: 'fetch after false data-if becomes visible',
|
|
983
|
-
});
|
|
984
|
-
expect(section.hasAttribute('data-if-false')).toBe(false);
|
|
985
|
-
expect(fetchSpy.mock.calls[0][0]).toBe('http://api.test/search?query=alpha');
|
|
986
|
-
expect(JSON.parse(bindTarget.getAttribute('data-bind') || '{}').query).toBe('alpha');
|
|
987
|
-
});
|
|
988
|
-
});
|
|
989
|
-
});
|
|
990
|
-
describe('data-fetch + data-each + data-if integration', () => {
|
|
991
|
-
let container;
|
|
992
|
-
beforeEach(() => {
|
|
993
|
-
container = document.createElement('div');
|
|
994
|
-
document.body.appendChild(container);
|
|
995
|
-
});
|
|
996
|
-
afterEach(() => {
|
|
997
|
-
document.body.removeChild(container);
|
|
998
|
-
vi.restoreAllMocks();
|
|
999
|
-
});
|
|
1000
|
-
it('data-fetch経由でデータを取得した場合もdata-each内のdata-ifが行ごとに正しく評価される', async () => {
|
|
1001
|
-
const responseData = {
|
|
1002
|
-
content: [
|
|
1003
|
-
{ id: 1, customerCode: 'C001', category: '顧客', billingId: null },
|
|
1004
|
-
{ id: 2, customerCode: 'C002', category: '請求', billingId: 'B001' },
|
|
1005
|
-
{ id: 3, customerCode: 'C003', category: '入金', billingId: null },
|
|
1006
|
-
],
|
|
1007
|
-
};
|
|
1008
|
-
vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(JSON.stringify(responseData), {
|
|
1009
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1010
|
-
}));
|
|
1011
|
-
container.innerHTML = `
|
|
1012
|
-
<section id="alert-list" data-fetch="/api/alerts.json">
|
|
1013
|
-
<table>
|
|
1014
|
-
<tbody data-each="content" data-each-key="id">
|
|
1015
|
-
<tr>
|
|
1016
|
-
<td>
|
|
1017
|
-
<a data-if="category === '顧客'" href="customer-list.html?customerCode={{customerCode}}">顧客対応</a>
|
|
1018
|
-
<a
|
|
1019
|
-
data-if="category === '請求'"
|
|
1020
|
-
href="billing-list.html?customerCode={{customerCode}}&billingId={{billingId}}"
|
|
1021
|
-
>請求対応</a>
|
|
1022
|
-
<a data-if="category === '入金'" href="payment-list.html?customerCode={{customerCode}}">入金対応</a>
|
|
1023
|
-
</td>
|
|
1024
|
-
</tr>
|
|
1025
|
-
</tbody>
|
|
1026
|
-
</table>
|
|
1027
|
-
</section>
|
|
1028
|
-
`;
|
|
1029
|
-
const section = container.querySelector('section');
|
|
1030
|
-
await Core.scan(section);
|
|
1031
|
-
const tbody = container.querySelector('tbody');
|
|
1032
|
-
await waitForCondition(() => tbody.querySelectorAll('tr').length === 3, { description: 'tbody rows via fetch' });
|
|
1033
|
-
await waitForDomSettled();
|
|
1034
|
-
expect(tbody.querySelectorAll('[data-if-false]').length).toBeGreaterThan(0);
|
|
1035
|
-
const rows = Array.from(tbody.querySelectorAll('tr'));
|
|
1036
|
-
const row0Links = Array.from(rows[0].querySelectorAll('a'));
|
|
1037
|
-
const row1Links = Array.from(rows[1].querySelectorAll('a'));
|
|
1038
|
-
const row2Links = Array.from(rows[2].querySelectorAll('a'));
|
|
1039
|
-
// row0: category=顧客 → 顧客リンクのみ表示、href が展開されていること
|
|
1040
|
-
expect(row0Links[0].hasAttribute('data-if-false')).toBe(false);
|
|
1041
|
-
expect(row0Links[0].getAttribute('href')).toBe('customer-list.html?customerCode=C001');
|
|
1042
|
-
expect(row0Links[1].hasAttribute('data-if-false')).toBe(true);
|
|
1043
|
-
expect(row0Links[2].hasAttribute('data-if-false')).toBe(true);
|
|
1044
|
-
// row1: category=請求 → 請求リンクのみ表示、href が展開されていること
|
|
1045
|
-
expect(row1Links[0].hasAttribute('data-if-false')).toBe(true);
|
|
1046
|
-
expect(row1Links[1].hasAttribute('data-if-false')).toBe(false);
|
|
1047
|
-
expect(row1Links[1].getAttribute('href')).toBe('billing-list.html?customerCode=C002&billingId=B001');
|
|
1048
|
-
expect(row1Links[2].hasAttribute('data-if-false')).toBe(true);
|
|
1049
|
-
// row2: category=入金 → 入金リンクのみ表示、href が展開されていること
|
|
1050
|
-
expect(row2Links[0].hasAttribute('data-if-false')).toBe(true);
|
|
1051
|
-
expect(row2Links[1].hasAttribute('data-if-false')).toBe(true);
|
|
1052
|
-
expect(row2Links[2].hasAttribute('data-if-false')).toBe(false);
|
|
1053
|
-
expect(row2Links[2].getAttribute('href')).toBe('payment-list.html?customerCode=C003');
|
|
1054
|
-
});
|
|
1055
|
-
});
|
|
1056
|
-
//# sourceMappingURL=core.test.js.map
|