zero-query 1.0.1 → 1.0.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.md +50 -9
- package/cli/help.js +2 -1
- package/cli/scaffold/default/app/app.js +3 -6
- package/cli/scaffold/default/global.css +12 -3
- package/cli/scaffold/default/index.html +8 -8
- package/cli/scaffold/minimal/app/app.js +3 -9
- package/cli/scaffold/minimal/global.css +12 -3
- package/cli/scaffold/minimal/index.html +3 -3
- package/cli/scaffold/ssr/app/app.js +18 -6
- package/cli/scaffold/ssr/app/components/about.js +42 -15
- package/cli/scaffold/ssr/app/components/blog/index.js +65 -0
- package/cli/scaffold/ssr/app/components/blog/post.js +86 -0
- package/cli/scaffold/ssr/app/routes.js +4 -2
- package/cli/scaffold/ssr/global.css +117 -1
- package/cli/scaffold/ssr/index.html +8 -2
- package/cli/scaffold/ssr/server/data/posts.js +144 -0
- package/cli/scaffold/ssr/server/index.js +117 -23
- package/dist/zquery.dist.zip +0 -0
- package/dist/zquery.js +85 -20
- package/dist/zquery.min.js +2 -2
- package/index.d.ts +5 -2
- package/index.js +5 -4
- package/package.json +1 -1
- package/src/component.js +17 -2
- package/src/router.js +61 -12
- package/src/ssr.js +100 -0
- package/tests/component.test.js +480 -0
- package/tests/router.test.js +65 -1
- package/tests/ssr.test.js +175 -0
- package/tests/test-minifier.js +4 -4
- package/types/misc.d.ts +23 -1
- package/types/router.d.ts +25 -0
- package/types/ssr.d.ts +33 -0
package/tests/component.test.js
CHANGED
|
@@ -2742,6 +2742,486 @@ describe('component - combined key + system modifiers', () => {
|
|
|
2742
2742
|
});
|
|
2743
2743
|
|
|
2744
2744
|
|
|
2745
|
+
// ===========================================================================
|
|
2746
|
+
// Dynamic key modifiers - arbitrary keys matched against e.key
|
|
2747
|
+
// ===========================================================================
|
|
2748
|
+
|
|
2749
|
+
describe('component - dynamic key modifier: single letter keys', () => {
|
|
2750
|
+
it('.a fires only on "a" key (case-insensitive)', () => {
|
|
2751
|
+
let count = 0;
|
|
2752
|
+
component('dkey-a', {
|
|
2753
|
+
handler() { count++; },
|
|
2754
|
+
render() { return '<input @keydown.a="handler">'; },
|
|
2755
|
+
});
|
|
2756
|
+
document.body.innerHTML = '<dkey-a id="dka"></dkey-a>';
|
|
2757
|
+
mount('#dka', 'dkey-a');
|
|
2758
|
+
const input = document.querySelector('#dka input');
|
|
2759
|
+
|
|
2760
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'a', bubbles: true }));
|
|
2761
|
+
expect(count).toBe(1);
|
|
2762
|
+
|
|
2763
|
+
// Uppercase A should also match (case-insensitive)
|
|
2764
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'A', bubbles: true }));
|
|
2765
|
+
expect(count).toBe(2);
|
|
2766
|
+
|
|
2767
|
+
// Other keys must NOT fire
|
|
2768
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'b', bubbles: true }));
|
|
2769
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Enter', bubbles: true }));
|
|
2770
|
+
expect(count).toBe(2);
|
|
2771
|
+
});
|
|
2772
|
+
|
|
2773
|
+
it('.z fires only on "z" key', () => {
|
|
2774
|
+
let count = 0;
|
|
2775
|
+
component('dkey-z', {
|
|
2776
|
+
handler() { count++; },
|
|
2777
|
+
render() { return '<input @keydown.z="handler">'; },
|
|
2778
|
+
});
|
|
2779
|
+
document.body.innerHTML = '<dkey-z id="dkz"></dkey-z>';
|
|
2780
|
+
mount('#dkz', 'dkey-z');
|
|
2781
|
+
const input = document.querySelector('#dkz input');
|
|
2782
|
+
|
|
2783
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'z', bubbles: true }));
|
|
2784
|
+
expect(count).toBe(1);
|
|
2785
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Z', bubbles: true }));
|
|
2786
|
+
expect(count).toBe(2);
|
|
2787
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'a', bubbles: true }));
|
|
2788
|
+
expect(count).toBe(2);
|
|
2789
|
+
});
|
|
2790
|
+
});
|
|
2791
|
+
|
|
2792
|
+
describe('component - dynamic key modifier: function keys', () => {
|
|
2793
|
+
it('.f1 fires only on F1', () => {
|
|
2794
|
+
let count = 0;
|
|
2795
|
+
component('dkey-f1', {
|
|
2796
|
+
handler() { count++; },
|
|
2797
|
+
render() { return '<div @keydown.f1="handler" tabindex="0">x</div>'; },
|
|
2798
|
+
});
|
|
2799
|
+
document.body.innerHTML = '<dkey-f1 id="dkf1"></dkey-f1>';
|
|
2800
|
+
mount('#dkf1', 'dkey-f1');
|
|
2801
|
+
const el = document.querySelector('#dkf1 div');
|
|
2802
|
+
|
|
2803
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'F1', bubbles: true }));
|
|
2804
|
+
expect(count).toBe(1);
|
|
2805
|
+
|
|
2806
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'F2', bubbles: true }));
|
|
2807
|
+
expect(count).toBe(1);
|
|
2808
|
+
});
|
|
2809
|
+
|
|
2810
|
+
it('.f12 fires only on F12', () => {
|
|
2811
|
+
let count = 0;
|
|
2812
|
+
component('dkey-f12', {
|
|
2813
|
+
handler() { count++; },
|
|
2814
|
+
render() { return '<div @keydown.f12="handler" tabindex="0">x</div>'; },
|
|
2815
|
+
});
|
|
2816
|
+
document.body.innerHTML = '<dkey-f12 id="dkf12"></dkey-f12>';
|
|
2817
|
+
mount('#dkf12', 'dkey-f12');
|
|
2818
|
+
const el = document.querySelector('#dkf12 div');
|
|
2819
|
+
|
|
2820
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'F12', bubbles: true }));
|
|
2821
|
+
expect(count).toBe(1);
|
|
2822
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'F1', bubbles: true }));
|
|
2823
|
+
expect(count).toBe(1);
|
|
2824
|
+
});
|
|
2825
|
+
});
|
|
2826
|
+
|
|
2827
|
+
describe('component - dynamic key modifier: numeric keys', () => {
|
|
2828
|
+
it('.0 fires only on "0" key (not confused with debounce/throttle ms)', () => {
|
|
2829
|
+
let count = 0;
|
|
2830
|
+
component('dkey-zero', {
|
|
2831
|
+
handler() { count++; },
|
|
2832
|
+
render() { return '<input @keydown.0="handler">'; },
|
|
2833
|
+
});
|
|
2834
|
+
document.body.innerHTML = '<dkey-zero id="dk0"></dkey-zero>';
|
|
2835
|
+
mount('#dk0', 'dkey-zero');
|
|
2836
|
+
const input = document.querySelector('#dk0 input');
|
|
2837
|
+
|
|
2838
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: '0', bubbles: true }));
|
|
2839
|
+
expect(count).toBe(1);
|
|
2840
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: '1', bubbles: true }));
|
|
2841
|
+
expect(count).toBe(1);
|
|
2842
|
+
});
|
|
2843
|
+
});
|
|
2844
|
+
|
|
2845
|
+
describe('component - dynamic key modifier: special/punctuation keys', () => {
|
|
2846
|
+
it('.+ fires on "+" key', () => {
|
|
2847
|
+
let count = 0;
|
|
2848
|
+
component('dkey-plus', {
|
|
2849
|
+
handler() { count++; },
|
|
2850
|
+
render() { return '<div @keydown.+="handler" tabindex="0">x</div>'; },
|
|
2851
|
+
});
|
|
2852
|
+
document.body.innerHTML = '<dkey-plus id="dkplus"></dkey-plus>';
|
|
2853
|
+
mount('#dkplus', 'dkey-plus');
|
|
2854
|
+
const el = document.querySelector('#dkplus div');
|
|
2855
|
+
|
|
2856
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: '+', bubbles: true }));
|
|
2857
|
+
expect(count).toBe(1);
|
|
2858
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: '-', bubbles: true }));
|
|
2859
|
+
expect(count).toBe(1);
|
|
2860
|
+
});
|
|
2861
|
+
|
|
2862
|
+
it('.- fires on "-" key', () => {
|
|
2863
|
+
let count = 0;
|
|
2864
|
+
component('dkey-minus', {
|
|
2865
|
+
handler() { count++; },
|
|
2866
|
+
render() { return '<div @keydown.-="handler" tabindex="0">x</div>'; },
|
|
2867
|
+
});
|
|
2868
|
+
document.body.innerHTML = '<dkey-minus id="dkminus"></dkey-minus>';
|
|
2869
|
+
mount('#dkminus', 'dkey-minus');
|
|
2870
|
+
const el = document.querySelector('#dkminus div');
|
|
2871
|
+
|
|
2872
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: '-', bubbles: true }));
|
|
2873
|
+
expect(count).toBe(1);
|
|
2874
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: '+', bubbles: true }));
|
|
2875
|
+
expect(count).toBe(1);
|
|
2876
|
+
});
|
|
2877
|
+
});
|
|
2878
|
+
|
|
2879
|
+
describe('component - dynamic key modifier combined with system modifiers', () => {
|
|
2880
|
+
it('.ctrl.s fires only on Ctrl+S', () => {
|
|
2881
|
+
let count = 0;
|
|
2882
|
+
component('dkey-ctrl-s', {
|
|
2883
|
+
handler() { count++; },
|
|
2884
|
+
render() { return '<div @keydown.ctrl.s="handler" tabindex="0">x</div>'; },
|
|
2885
|
+
});
|
|
2886
|
+
document.body.innerHTML = '<dkey-ctrl-s id="dkcs"></dkey-ctrl-s>';
|
|
2887
|
+
mount('#dkcs', 'dkey-ctrl-s');
|
|
2888
|
+
const el = document.querySelector('#dkcs div');
|
|
2889
|
+
|
|
2890
|
+
// S without Ctrl → no fire
|
|
2891
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: 's', bubbles: true, ctrlKey: false }));
|
|
2892
|
+
expect(count).toBe(0);
|
|
2893
|
+
|
|
2894
|
+
// Ctrl without S → no fire
|
|
2895
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'a', bubbles: true, ctrlKey: true }));
|
|
2896
|
+
expect(count).toBe(0);
|
|
2897
|
+
|
|
2898
|
+
// Ctrl+S → fire
|
|
2899
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: 's', bubbles: true, ctrlKey: true }));
|
|
2900
|
+
expect(count).toBe(1);
|
|
2901
|
+
|
|
2902
|
+
// Ctrl+Shift+S (uppercase) → also fires (case insensitive)
|
|
2903
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'S', bubbles: true, ctrlKey: true }));
|
|
2904
|
+
expect(count).toBe(2);
|
|
2905
|
+
});
|
|
2906
|
+
|
|
2907
|
+
it('.meta.k fires only on Meta+K', () => {
|
|
2908
|
+
let count = 0;
|
|
2909
|
+
component('dkey-meta-k', {
|
|
2910
|
+
handler() { count++; },
|
|
2911
|
+
render() { return '<div @keydown.meta.k="handler" tabindex="0">x</div>'; },
|
|
2912
|
+
});
|
|
2913
|
+
document.body.innerHTML = '<dkey-meta-k id="dkmk"></dkey-meta-k>';
|
|
2914
|
+
mount('#dkmk', 'dkey-meta-k');
|
|
2915
|
+
const el = document.querySelector('#dkmk div');
|
|
2916
|
+
|
|
2917
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', bubbles: true, metaKey: false }));
|
|
2918
|
+
expect(count).toBe(0);
|
|
2919
|
+
|
|
2920
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'k', bubbles: true, metaKey: true }));
|
|
2921
|
+
expect(count).toBe(1);
|
|
2922
|
+
});
|
|
2923
|
+
|
|
2924
|
+
it('.alt.shift.f fires only on Alt+Shift+F', () => {
|
|
2925
|
+
let count = 0;
|
|
2926
|
+
component('dkey-alt-sf', {
|
|
2927
|
+
handler() { count++; },
|
|
2928
|
+
render() { return '<div @keydown.alt.shift.f="handler" tabindex="0">x</div>'; },
|
|
2929
|
+
});
|
|
2930
|
+
document.body.innerHTML = '<dkey-alt-sf id="dkas"></dkey-alt-sf>';
|
|
2931
|
+
mount('#dkas', 'dkey-alt-sf');
|
|
2932
|
+
const el = document.querySelector('#dkas div');
|
|
2933
|
+
|
|
2934
|
+
// Missing shift
|
|
2935
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'f', bubbles: true, altKey: true, shiftKey: false }));
|
|
2936
|
+
expect(count).toBe(0);
|
|
2937
|
+
|
|
2938
|
+
// Missing alt
|
|
2939
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'f', bubbles: true, altKey: false, shiftKey: true }));
|
|
2940
|
+
expect(count).toBe(0);
|
|
2941
|
+
|
|
2942
|
+
// Both held
|
|
2943
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'f', bubbles: true, altKey: true, shiftKey: true }));
|
|
2944
|
+
expect(count).toBe(1);
|
|
2945
|
+
});
|
|
2946
|
+
});
|
|
2947
|
+
|
|
2948
|
+
describe('component - dynamic key modifier combined with behaviour modifiers', () => {
|
|
2949
|
+
it('.a.prevent calls preventDefault only on "a" key', () => {
|
|
2950
|
+
component('dkey-a-prev', {
|
|
2951
|
+
handler() {},
|
|
2952
|
+
render() { return '<input @keydown.a.prevent="handler">'; },
|
|
2953
|
+
});
|
|
2954
|
+
document.body.innerHTML = '<dkey-a-prev id="dkap"></dkey-a-prev>';
|
|
2955
|
+
mount('#dkap', 'dkey-a-prev');
|
|
2956
|
+
const input = document.querySelector('#dkap input');
|
|
2957
|
+
|
|
2958
|
+
const aEvt = new KeyboardEvent('keydown', { key: 'a', bubbles: true, cancelable: true });
|
|
2959
|
+
vi.spyOn(aEvt, 'preventDefault');
|
|
2960
|
+
input.dispatchEvent(aEvt);
|
|
2961
|
+
expect(aEvt.preventDefault).toHaveBeenCalled();
|
|
2962
|
+
|
|
2963
|
+
const bEvt = new KeyboardEvent('keydown', { key: 'b', bubbles: true, cancelable: true });
|
|
2964
|
+
vi.spyOn(bEvt, 'preventDefault');
|
|
2965
|
+
input.dispatchEvent(bEvt);
|
|
2966
|
+
expect(bEvt.preventDefault).not.toHaveBeenCalled();
|
|
2967
|
+
});
|
|
2968
|
+
|
|
2969
|
+
it('.ctrl.s.prevent.stop calls both preventDefault and stopPropagation', () => {
|
|
2970
|
+
component('dkey-cs-ps', {
|
|
2971
|
+
handler() {},
|
|
2972
|
+
render() { return '<div @keydown.ctrl.s.prevent.stop="handler" tabindex="0">x</div>'; },
|
|
2973
|
+
});
|
|
2974
|
+
document.body.innerHTML = '<dkey-cs-ps id="dkcsps"></dkey-cs-ps>';
|
|
2975
|
+
mount('#dkcsps', 'dkey-cs-ps');
|
|
2976
|
+
const el = document.querySelector('#dkcsps div');
|
|
2977
|
+
|
|
2978
|
+
const evt = new KeyboardEvent('keydown', { key: 's', bubbles: true, cancelable: true, ctrlKey: true });
|
|
2979
|
+
vi.spyOn(evt, 'preventDefault');
|
|
2980
|
+
vi.spyOn(evt, 'stopPropagation');
|
|
2981
|
+
el.dispatchEvent(evt);
|
|
2982
|
+
expect(evt.preventDefault).toHaveBeenCalled();
|
|
2983
|
+
expect(evt.stopPropagation).toHaveBeenCalled();
|
|
2984
|
+
});
|
|
2985
|
+
});
|
|
2986
|
+
|
|
2987
|
+
describe('component - dynamic key modifier: named shortcuts still work', () => {
|
|
2988
|
+
it('named shortcuts take priority over dynamic matching', () => {
|
|
2989
|
+
// .space maps to ' ' (literal space char) via _keyMap, NOT "space" string
|
|
2990
|
+
let count = 0;
|
|
2991
|
+
component('dkey-space-prio', {
|
|
2992
|
+
handler() { count++; },
|
|
2993
|
+
render() { return '<button @keydown.space="handler">x</button>'; },
|
|
2994
|
+
});
|
|
2995
|
+
document.body.innerHTML = '<dkey-space-prio id="dksp"></dkey-space-prio>';
|
|
2996
|
+
mount('#dksp', 'dkey-space-prio');
|
|
2997
|
+
const btn = document.querySelector('#dksp button');
|
|
2998
|
+
|
|
2999
|
+
// ' ' is the actual e.key for Space
|
|
3000
|
+
btn.dispatchEvent(new KeyboardEvent('keydown', { key: ' ', bubbles: true }));
|
|
3001
|
+
expect(count).toBe(1);
|
|
3002
|
+
|
|
3003
|
+
// 'space' is NOT the e.key - should NOT fire because .space uses _keyMap
|
|
3004
|
+
btn.dispatchEvent(new KeyboardEvent('keydown', { key: 'space', bubbles: true }));
|
|
3005
|
+
expect(count).toBe(1);
|
|
3006
|
+
});
|
|
3007
|
+
|
|
3008
|
+
it('.delete still matches both Delete and Backspace', () => {
|
|
3009
|
+
let count = 0;
|
|
3010
|
+
component('dkey-del-prio', {
|
|
3011
|
+
handler() { count++; },
|
|
3012
|
+
render() { return '<input @keydown.delete="handler">'; },
|
|
3013
|
+
});
|
|
3014
|
+
document.body.innerHTML = '<dkey-del-prio id="dkdp"></dkey-del-prio>';
|
|
3015
|
+
mount('#dkdp', 'dkey-del-prio');
|
|
3016
|
+
const input = document.querySelector('#dkdp input');
|
|
3017
|
+
|
|
3018
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Delete', bubbles: true }));
|
|
3019
|
+
expect(count).toBe(1);
|
|
3020
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Backspace', bubbles: true }));
|
|
3021
|
+
expect(count).toBe(2);
|
|
3022
|
+
});
|
|
3023
|
+
});
|
|
3024
|
+
|
|
3025
|
+
describe('component - dynamic key modifier: no-key events are not filtered', () => {
|
|
3026
|
+
it('click events pass through dynamic key modifiers without e.key', () => {
|
|
3027
|
+
// A .prevent on a click should not be treated as a key filter
|
|
3028
|
+
let count = 0;
|
|
3029
|
+
component('dkey-click-ok', {
|
|
3030
|
+
handler() { count++; },
|
|
3031
|
+
render() { return '<button @click.prevent="handler">x</button>'; },
|
|
3032
|
+
});
|
|
3033
|
+
document.body.innerHTML = '<dkey-click-ok id="dkco"></dkey-click-ok>';
|
|
3034
|
+
mount('#dkco', 'dkey-click-ok');
|
|
3035
|
+
const btn = document.querySelector('#dkco button');
|
|
3036
|
+
|
|
3037
|
+
btn.click();
|
|
3038
|
+
expect(count).toBe(1);
|
|
3039
|
+
});
|
|
3040
|
+
});
|
|
3041
|
+
|
|
3042
|
+
describe('component - dynamic key modifier: debounce/throttle ms values not treated as keys', () => {
|
|
3043
|
+
it('.debounce.300 does not treat "300" as a key filter', async () => {
|
|
3044
|
+
let count = 0;
|
|
3045
|
+
component('dkey-deb-num', {
|
|
3046
|
+
handler() { count++; },
|
|
3047
|
+
render() { return '<input @input.debounce.300="handler">'; },
|
|
3048
|
+
});
|
|
3049
|
+
document.body.innerHTML = '<dkey-deb-num id="dkdn"></dkey-deb-num>';
|
|
3050
|
+
mount('#dkdn', 'dkey-deb-num');
|
|
3051
|
+
const input = document.querySelector('#dkdn input');
|
|
3052
|
+
|
|
3053
|
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
|
3054
|
+
// Should be debounced, not blocked by "300" key filter
|
|
3055
|
+
await new Promise(r => setTimeout(r, 350));
|
|
3056
|
+
expect(count).toBe(1);
|
|
3057
|
+
});
|
|
3058
|
+
});
|
|
3059
|
+
|
|
3060
|
+
describe('component - dynamic key modifier: multiple dynamic keys on separate bindings', () => {
|
|
3061
|
+
it('separate @keydown.a and @keydown.b fire independently', () => {
|
|
3062
|
+
let aCount = 0, bCount = 0;
|
|
3063
|
+
component('dkey-ab', {
|
|
3064
|
+
handlerA() { aCount++; },
|
|
3065
|
+
handlerB() { bCount++; },
|
|
3066
|
+
render() { return '<input @keydown.a="handlerA" @keydown.b="handlerB">'; },
|
|
3067
|
+
});
|
|
3068
|
+
document.body.innerHTML = '<dkey-ab id="dkab"></dkey-ab>';
|
|
3069
|
+
mount('#dkab', 'dkey-ab');
|
|
3070
|
+
const input = document.querySelector('#dkab input');
|
|
3071
|
+
|
|
3072
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'a', bubbles: true }));
|
|
3073
|
+
expect(aCount).toBe(1);
|
|
3074
|
+
expect(bCount).toBe(0);
|
|
3075
|
+
|
|
3076
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'b', bubbles: true }));
|
|
3077
|
+
expect(aCount).toBe(1);
|
|
3078
|
+
expect(bCount).toBe(1);
|
|
3079
|
+
|
|
3080
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'c', bubbles: true }));
|
|
3081
|
+
expect(aCount).toBe(1);
|
|
3082
|
+
expect(bCount).toBe(1);
|
|
3083
|
+
});
|
|
3084
|
+
});
|
|
3085
|
+
|
|
3086
|
+
describe('component - dynamic key modifier: keyup vs keydown', () => {
|
|
3087
|
+
it('.a works on keyup events too', () => {
|
|
3088
|
+
let downCount = 0, upCount = 0;
|
|
3089
|
+
component('dkey-updown', {
|
|
3090
|
+
onDown() { downCount++; },
|
|
3091
|
+
onUp() { upCount++; },
|
|
3092
|
+
render() { return '<input @keydown.a="onDown" @keyup.a="onUp">'; },
|
|
3093
|
+
});
|
|
3094
|
+
document.body.innerHTML = '<dkey-updown id="dkud"></dkey-updown>';
|
|
3095
|
+
mount('#dkud', 'dkey-updown');
|
|
3096
|
+
const input = document.querySelector('#dkud input');
|
|
3097
|
+
|
|
3098
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'a', bubbles: true }));
|
|
3099
|
+
expect(downCount).toBe(1);
|
|
3100
|
+
expect(upCount).toBe(0);
|
|
3101
|
+
|
|
3102
|
+
input.dispatchEvent(new KeyboardEvent('keyup', { key: 'a', bubbles: true }));
|
|
3103
|
+
expect(downCount).toBe(1);
|
|
3104
|
+
expect(upCount).toBe(1);
|
|
3105
|
+
|
|
3106
|
+
// Wrong key on both → neither fires
|
|
3107
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'b', bubbles: true }));
|
|
3108
|
+
input.dispatchEvent(new KeyboardEvent('keyup', { key: 'b', bubbles: true }));
|
|
3109
|
+
expect(downCount).toBe(1);
|
|
3110
|
+
expect(upCount).toBe(1);
|
|
3111
|
+
});
|
|
3112
|
+
});
|
|
3113
|
+
|
|
3114
|
+
describe('component - dynamic key modifier: edge cases with e.key', () => {
|
|
3115
|
+
it('event without e.key property does not fire dynamic key handler', () => {
|
|
3116
|
+
let count = 0;
|
|
3117
|
+
component('dkey-nokey', {
|
|
3118
|
+
handler() { count++; },
|
|
3119
|
+
render() { return '<div @keydown.a="handler" tabindex="0">x</div>'; },
|
|
3120
|
+
});
|
|
3121
|
+
document.body.innerHTML = '<dkey-nokey id="dknk"></dkey-nokey>';
|
|
3122
|
+
mount('#dknk', 'dkey-nokey');
|
|
3123
|
+
const el = document.querySelector('#dknk div');
|
|
3124
|
+
|
|
3125
|
+
// KeyboardEvent with no key specified → e.key is empty string
|
|
3126
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { bubbles: true }));
|
|
3127
|
+
expect(count).toBe(0);
|
|
3128
|
+
});
|
|
3129
|
+
|
|
3130
|
+
it('case-insensitive match for multi-char keys like PageDown', () => {
|
|
3131
|
+
let count = 0;
|
|
3132
|
+
component('dkey-pgdn', {
|
|
3133
|
+
handler() { count++; },
|
|
3134
|
+
render() { return '<div @keydown.pagedown="handler" tabindex="0">x</div>'; },
|
|
3135
|
+
});
|
|
3136
|
+
document.body.innerHTML = '<dkey-pgdn id="dkpd"></dkey-pgdn>';
|
|
3137
|
+
mount('#dkpd', 'dkey-pgdn');
|
|
3138
|
+
const el = document.querySelector('#dkpd div');
|
|
3139
|
+
|
|
3140
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'PageDown', bubbles: true }));
|
|
3141
|
+
expect(count).toBe(1);
|
|
3142
|
+
|
|
3143
|
+
el.dispatchEvent(new KeyboardEvent('keydown', { key: 'PageUp', bubbles: true }));
|
|
3144
|
+
expect(count).toBe(1);
|
|
3145
|
+
});
|
|
3146
|
+
|
|
3147
|
+
it('.home and .end match Home / End keys', () => {
|
|
3148
|
+
let homeCount = 0, endCount = 0;
|
|
3149
|
+
component('dkey-homeend', {
|
|
3150
|
+
onHome() { homeCount++; },
|
|
3151
|
+
onEnd() { endCount++; },
|
|
3152
|
+
render() { return '<input @keydown.home="onHome" @keydown.end="onEnd">'; },
|
|
3153
|
+
});
|
|
3154
|
+
document.body.innerHTML = '<dkey-homeend id="dkhe"></dkey-homeend>';
|
|
3155
|
+
mount('#dkhe', 'dkey-homeend');
|
|
3156
|
+
const input = document.querySelector('#dkhe input');
|
|
3157
|
+
|
|
3158
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Home', bubbles: true }));
|
|
3159
|
+
expect(homeCount).toBe(1);
|
|
3160
|
+
expect(endCount).toBe(0);
|
|
3161
|
+
|
|
3162
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'End', bubbles: true }));
|
|
3163
|
+
expect(homeCount).toBe(1);
|
|
3164
|
+
expect(endCount).toBe(1);
|
|
3165
|
+
});
|
|
3166
|
+
|
|
3167
|
+
it('.insert matches Insert key', () => {
|
|
3168
|
+
let count = 0;
|
|
3169
|
+
component('dkey-ins', {
|
|
3170
|
+
handler() { count++; },
|
|
3171
|
+
render() { return '<input @keydown.insert="handler">'; },
|
|
3172
|
+
});
|
|
3173
|
+
document.body.innerHTML = '<dkey-ins id="dkins"></dkey-ins>';
|
|
3174
|
+
mount('#dkins', 'dkey-ins');
|
|
3175
|
+
const input = document.querySelector('#dkins input');
|
|
3176
|
+
|
|
3177
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Insert', bubbles: true }));
|
|
3178
|
+
expect(count).toBe(1);
|
|
3179
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'Delete', bubbles: true }));
|
|
3180
|
+
expect(count).toBe(1);
|
|
3181
|
+
});
|
|
3182
|
+
});
|
|
3183
|
+
|
|
3184
|
+
describe('component - dynamic key modifier: non-interfering with existing modifiers', () => {
|
|
3185
|
+
it('.once still works with dynamic key', () => {
|
|
3186
|
+
let count = 0;
|
|
3187
|
+
component('dkey-once-a', {
|
|
3188
|
+
handler() { count++; },
|
|
3189
|
+
render() { return '<input @keydown.a.once="handler">'; },
|
|
3190
|
+
});
|
|
3191
|
+
document.body.innerHTML = '<dkey-once-a id="dkoa"></dkey-once-a>';
|
|
3192
|
+
mount('#dkoa', 'dkey-once-a');
|
|
3193
|
+
const input = document.querySelector('#dkoa input');
|
|
3194
|
+
|
|
3195
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'a', bubbles: true }));
|
|
3196
|
+
expect(count).toBe(1);
|
|
3197
|
+
|
|
3198
|
+
// Second press should NOT fire (.once)
|
|
3199
|
+
input.dispatchEvent(new KeyboardEvent('keydown', { key: 'a', bubbles: true }));
|
|
3200
|
+
expect(count).toBe(1);
|
|
3201
|
+
});
|
|
3202
|
+
|
|
3203
|
+
it('.self still works with dynamic key', () => {
|
|
3204
|
+
let count = 0;
|
|
3205
|
+
component('dkey-self-a', {
|
|
3206
|
+
handler() { count++; },
|
|
3207
|
+
render() { return '<div @keydown.a.self="handler" tabindex="0"><span>child</span></div>'; },
|
|
3208
|
+
});
|
|
3209
|
+
document.body.innerHTML = '<dkey-self-a id="dksa"></dkey-self-a>';
|
|
3210
|
+
mount('#dksa', 'dkey-self-a');
|
|
3211
|
+
const div = document.querySelector('#dksa div');
|
|
3212
|
+
const span = document.querySelector('#dksa span');
|
|
3213
|
+
|
|
3214
|
+
// Fire on div itself → should work
|
|
3215
|
+
div.dispatchEvent(new KeyboardEvent('keydown', { key: 'a', bubbles: true }));
|
|
3216
|
+
expect(count).toBe(1);
|
|
3217
|
+
|
|
3218
|
+
// Fire from child → should NOT fire (.self)
|
|
3219
|
+
span.dispatchEvent(new KeyboardEvent('keydown', { key: 'a', bubbles: true }));
|
|
3220
|
+
expect(count).toBe(1);
|
|
3221
|
+
});
|
|
3222
|
+
});
|
|
3223
|
+
|
|
3224
|
+
|
|
2745
3225
|
// ===========================================================================
|
|
2746
3226
|
// .outside modifier - fire when event target is outside the element
|
|
2747
3227
|
// ===========================================================================
|
package/tests/router.test.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { createRouter, getRouter } from '../src/router.js';
|
|
2
|
+
import { createRouter, getRouter, matchRoute } from '../src/router.js';
|
|
3
3
|
import { component } from '../src/component.js';
|
|
4
4
|
|
|
5
5
|
// Register stub components used in route definitions so mount() doesn't throw
|
|
@@ -809,6 +809,70 @@ describe('Router - navigation chaining', () => {
|
|
|
809
809
|
});
|
|
810
810
|
|
|
811
811
|
|
|
812
|
+
// ---------------------------------------------------------------------------
|
|
813
|
+
// matchRoute - standalone route matcher (DOM-free)
|
|
814
|
+
// ---------------------------------------------------------------------------
|
|
815
|
+
|
|
816
|
+
describe('matchRoute', () => {
|
|
817
|
+
const routes = [
|
|
818
|
+
{ path: '/', component: 'home-page' },
|
|
819
|
+
{ path: '/blog', component: 'blog-list' },
|
|
820
|
+
{ path: '/blog/:slug', component: 'blog-post' },
|
|
821
|
+
{ path: '/user/:id', component: 'user-page' },
|
|
822
|
+
{ path: '/files/*', component: 'file-browser' },
|
|
823
|
+
];
|
|
824
|
+
|
|
825
|
+
it('matches a static route', () => {
|
|
826
|
+
expect(matchRoute(routes, '/')).toEqual({ component: 'home-page', params: {} });
|
|
827
|
+
expect(matchRoute(routes, '/blog')).toEqual({ component: 'blog-list', params: {} });
|
|
828
|
+
});
|
|
829
|
+
|
|
830
|
+
it('matches a parameterized route', () => {
|
|
831
|
+
expect(matchRoute(routes, '/blog/hello-world')).toEqual({
|
|
832
|
+
component: 'blog-post',
|
|
833
|
+
params: { slug: 'hello-world' },
|
|
834
|
+
});
|
|
835
|
+
});
|
|
836
|
+
|
|
837
|
+
it('matches multiple params', () => {
|
|
838
|
+
const r = [{ path: '/user/:id/post/:pid', component: 'user-post' }];
|
|
839
|
+
expect(matchRoute(r, '/user/42/post/7')).toEqual({
|
|
840
|
+
component: 'user-post',
|
|
841
|
+
params: { id: '42', pid: '7' },
|
|
842
|
+
});
|
|
843
|
+
});
|
|
844
|
+
|
|
845
|
+
it('matches wildcard routes', () => {
|
|
846
|
+
const result = matchRoute(routes, '/files/docs/readme.md');
|
|
847
|
+
expect(result.component).toBe('file-browser');
|
|
848
|
+
});
|
|
849
|
+
|
|
850
|
+
it('returns fallback when nothing matches', () => {
|
|
851
|
+
expect(matchRoute(routes, '/nope')).toEqual({ component: 'not-found', params: {} });
|
|
852
|
+
});
|
|
853
|
+
|
|
854
|
+
it('accepts a custom fallback component name', () => {
|
|
855
|
+
expect(matchRoute(routes, '/nope', '404-page')).toEqual({ component: '404-page', params: {} });
|
|
856
|
+
});
|
|
857
|
+
|
|
858
|
+
it('matches first route when multiple could match', () => {
|
|
859
|
+
const r = [
|
|
860
|
+
{ path: '/a', component: 'first' },
|
|
861
|
+
{ path: '/a', component: 'second' },
|
|
862
|
+
];
|
|
863
|
+
expect(matchRoute(r, '/a').component).toBe('first');
|
|
864
|
+
});
|
|
865
|
+
|
|
866
|
+
it('handles per-route fallback aliases', () => {
|
|
867
|
+
const r = [
|
|
868
|
+
{ path: '/docs/:section', component: 'docs-page', fallback: '/docs' },
|
|
869
|
+
];
|
|
870
|
+
expect(matchRoute(r, '/docs/intro')).toEqual({ component: 'docs-page', params: { section: 'intro' } });
|
|
871
|
+
expect(matchRoute(r, '/docs')).toEqual({ component: 'docs-page', params: {} });
|
|
872
|
+
});
|
|
873
|
+
});
|
|
874
|
+
|
|
875
|
+
|
|
812
876
|
// ---------------------------------------------------------------------------
|
|
813
877
|
// Hash mode path parsing
|
|
814
878
|
// ---------------------------------------------------------------------------
|