@mantou/gem 2.2.14 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/dist/gem.js +7 -7
  2. package/dist/gem.mjs +7 -7
  3. package/elements/base/link.d.ts.map +1 -1
  4. package/elements/base/link.js +6 -3
  5. package/elements/base/link.js.map +1 -1
  6. package/elements/base/modal-factory.d.ts +3 -2
  7. package/elements/base/modal-factory.d.ts.map +1 -1
  8. package/elements/base/route.d.ts +8 -3
  9. package/elements/base/route.d.ts.map +1 -1
  10. package/elements/base/route.js +84 -44
  11. package/elements/base/route.js.map +1 -1
  12. package/elements/base/use.d.ts.map +1 -1
  13. package/elements/base/use.js +11 -0
  14. package/elements/base/use.js.map +1 -1
  15. package/helper/hmr.d.ts +3 -2
  16. package/helper/hmr.d.ts.map +1 -1
  17. package/helper/hmr.js +95 -23
  18. package/helper/hmr.js.map +1 -1
  19. package/helper/i18n.d.ts +2 -1
  20. package/helper/i18n.d.ts.map +1 -1
  21. package/helper/i18n.js +9 -1
  22. package/helper/i18n.js.map +1 -1
  23. package/helper/theme.d.ts +8 -4
  24. package/helper/theme.d.ts.map +1 -1
  25. package/helper/theme.js +15 -10
  26. package/helper/theme.js.map +1 -1
  27. package/lib/decorators.d.ts.map +1 -1
  28. package/lib/decorators.js +31 -46
  29. package/lib/decorators.js.map +1 -1
  30. package/lib/history.d.ts +10 -7
  31. package/lib/history.d.ts.map +1 -1
  32. package/lib/history.js +37 -51
  33. package/lib/history.js.map +1 -1
  34. package/lib/lit-html.d.ts +15 -0
  35. package/lib/lit-html.d.ts.map +1 -1
  36. package/lib/lit-html.js +3 -1
  37. package/lib/lit-html.js.map +1 -1
  38. package/lib/reactive.d.ts +6 -2
  39. package/lib/reactive.d.ts.map +1 -1
  40. package/lib/reactive.js +114 -118
  41. package/lib/reactive.js.map +1 -1
  42. package/lib/server.d.ts +26 -0
  43. package/lib/server.d.ts.map +1 -0
  44. package/lib/server.js +45 -0
  45. package/lib/server.js.map +1 -0
  46. package/lib/store.d.ts +4 -1
  47. package/lib/store.d.ts.map +1 -1
  48. package/lib/store.js +24 -9
  49. package/lib/store.js.map +1 -1
  50. package/lib/utils.d.ts +2 -2
  51. package/lib/utils.d.ts.map +1 -1
  52. package/lib/utils.js +20 -7
  53. package/lib/utils.js.map +1 -1
  54. package/lib/version.d.ts +1 -1
  55. package/lib/version.d.ts.map +1 -1
  56. package/lib/version.js +1 -1
  57. package/lib/version.js.map +1 -1
  58. package/package.json +3 -4
  59. package/src/elements/base/link.ts +5 -3
  60. package/src/elements/base/route.ts +84 -49
  61. package/src/elements/base/use.ts +11 -0
  62. package/src/helper/hmr.ts +105 -37
  63. package/src/helper/i18n.ts +12 -1
  64. package/src/helper/theme.ts +19 -10
  65. package/src/lib/decorators.ts +38 -51
  66. package/src/lib/history.ts +37 -46
  67. package/src/lib/lit-html.ts +3 -1
  68. package/src/lib/reactive.ts +115 -124
  69. package/src/lib/server.ts +59 -0
  70. package/src/lib/store.ts +24 -9
  71. package/src/lib/utils.ts +22 -7
  72. package/src/lib/version.ts +1 -1
  73. package/src/test/elements/gesture.test.ts +13 -5
  74. package/src/test/elements/route.test.ts +102 -0
  75. package/src/test/elements/title.test.ts +7 -0
  76. package/src/test/gem-element/advance.test.ts +19 -0
  77. package/src/test/gem-element/basic.test.ts +33 -0
  78. package/src/test/history.test.ts +19 -0
  79. package/src/test/store.test.ts +12 -0
  80. package/src/test/theme.test.ts +8 -1
  81. package/src/test/utils.test.ts +6 -0
  82. package/src/test/utils.ts +56 -0
@@ -0,0 +1,102 @@
1
+ import type { GemRouteElement } from '../../elements/route';
2
+ import { html } from '../../lib/element';
3
+ import { history } from '../../lib/history';
4
+ import { expect, fixture, nextFrame, waitForPopState } from '../utils';
5
+
6
+ import '../../elements/route';
7
+
8
+ const routes = [{ pattern: '*', content: html`<div style="height: 1000px"></div>` }];
9
+
10
+ // 滚动位置在路由渲染后更新,恢复位置可能晚一个微任务,等待 `routechange` 后已经更新
11
+ const rendered = (route: GemRouteElement) =>
12
+ new Promise((resolve) => route.addEventListener('routechange', resolve, { once: true }));
13
+
14
+ let container: HTMLElement;
15
+
16
+ async function createRoute() {
17
+ container = await fixture(
18
+ html`<div style="height: 100px; overflow: auto"><gem-route .routes=${routes}></gem-route></div>`,
19
+ );
20
+ return container.querySelector<GemRouteElement>('gem-route')!;
21
+ }
22
+
23
+ async function navigate(route: GemRouteElement, action: () => void) {
24
+ const promise = rendered(route);
25
+ action();
26
+ await promise;
27
+ }
28
+
29
+ describe('<gem-route> scrollContainer', () => {
30
+ // 所有路由共享滚动位置,避免之前的路由记录位置
31
+ afterEach(() => container.remove());
32
+
33
+ it('导航时滚动到顶部,后退时恢复位置,同一条历史记录重新渲染时保持位置', async () => {
34
+ const route = await createRoute();
35
+ route.scrollContainer = container;
36
+ await nextFrame();
37
+ await navigate(route, () => history.push({ path: '/scroll-a' }));
38
+ container.scrollTop = 300;
39
+ await navigate(route, () => history.push({ path: '/scroll-b' }));
40
+ expect(container.scrollTop).to.equal(0);
41
+ container.scrollTop = 100;
42
+ await navigate(route, () => (route.key = 1));
43
+ expect(container.scrollTop).to.equal(100);
44
+ await navigate(route, () => waitForPopState(() => history.back()));
45
+ expect(container.scrollTop).to.equal(300);
46
+ });
47
+
48
+ it('前进后退时位置未知则滚动到顶部', async () => {
49
+ const route = await createRoute();
50
+ await navigate(route, () => history.push({ path: '/scroll-c' }));
51
+ await navigate(route, () => history.push({ path: '/scroll-d' }));
52
+ route.scrollContainer = container;
53
+ await nextFrame();
54
+ container.scrollTop = 50;
55
+ await navigate(route, () => waitForPopState(() => history.back()));
56
+ expect(container.scrollTop).to.equal(0);
57
+ });
58
+
59
+ it('前进后退时位置未知则保持文档的滚动位置', async () => {
60
+ // 避免浏览器恢复滚动位置
61
+ window.history.scrollRestoration = 'manual';
62
+ document.body.style.minHeight = '3000px';
63
+ const route = await createRoute();
64
+ await navigate(route, () => history.push({ path: '/scroll-f' }));
65
+ await navigate(route, () => history.push({ path: '/scroll-g' }));
66
+ route.scrollContainer = document.scrollingElement!;
67
+ await nextFrame();
68
+ scrollTo(0, 50);
69
+ await navigate(route, () => waitForPopState(() => history.back()));
70
+ expect(scrollY).to.equal(50);
71
+ scrollTo(0, 0);
72
+ document.body.style.minHeight = '';
73
+ window.history.scrollRestoration = 'auto';
74
+ });
75
+
76
+ it('恢复位置前等待 scrollWait', async () => {
77
+ const route = await createRoute();
78
+ route.scrollContainer = container;
79
+ let resolve = () => {};
80
+ const ready = new Promise<void>((r) => (resolve = r));
81
+ route.scrollWait = () => ready;
82
+ await nextFrame();
83
+ await navigate(route, () => history.push({ path: '/scroll-h' }));
84
+ container.scrollTop = 300;
85
+ await navigate(route, () => history.push({ path: '/scroll-i' }));
86
+ expect(container.scrollTop).to.equal(0);
87
+ await navigate(route, () => waitForPopState(() => history.back()));
88
+ expect(container.scrollTop).to.equal(0);
89
+ resolve();
90
+ await ready;
91
+ expect(container.scrollTop).to.equal(300);
92
+ });
93
+
94
+ it('滚动容器为空时不滚动', async () => {
95
+ const route = await createRoute();
96
+ route.scrollContainer = () => null;
97
+ await nextFrame();
98
+ container.scrollTop = 80;
99
+ await navigate(route, () => history.push({ path: '/scroll-e' }));
100
+ expect(container.scrollTop).to.equal(80);
101
+ });
102
+ });
@@ -21,5 +21,12 @@ describe('GemTitleElement', () => {
21
21
  await nextFrame();
22
22
  expect(document.title).to.equal('prefix-Page');
23
23
  expect(ele.shadowRoot!.textContent).to.equal('Page');
24
+
25
+ // 只改变 hash 时保持标题,不重复添加前缀
26
+ location.hash = '#a';
27
+ await nextFrame();
28
+ await nextFrame();
29
+ expect(document.title).to.equal('prefix-Page');
30
+ expect(ele.shadowRoot!.textContent).to.equal('Page');
24
31
  });
25
32
  });
@@ -106,12 +106,14 @@ class LifecycleGemElement extends GemElement {
106
106
  }
107
107
  renderCount = 0;
108
108
  mountedCount = 0;
109
+ unmountedCount = 0;
109
110
 
110
111
  @mounted()
111
112
  #init = () => {
112
113
  this.mountedCount++;
113
114
  return () => {
114
115
  this.renderCount = 0;
116
+ this.unmountedCount++;
115
117
  };
116
118
  };
117
119
 
@@ -155,10 +157,12 @@ describe('gem element 生命周期', () => {
155
157
  expect(el2.renderCount).to.equal(1);
156
158
  el2.remove();
157
159
  expect(el2.mountedCount).to.equal(1);
160
+ expect(el2.unmountedCount).to.equal(1);
158
161
  expect(el2.renderCount).to.equal(0);
159
162
  container.append(el2);
160
163
  await Promise.resolve();
161
164
  expect(el2.mountedCount).to.equal(2);
165
+ expect(el2.unmountedCount).to.equal(1, '重新挂载时不再执行已经执行过的清理函数');
162
166
  expect(el2.renderCount).to.equal(1);
163
167
  el2.update();
164
168
  el2.update();
@@ -169,8 +173,13 @@ describe('gem element 生命周期', () => {
169
173
  expect(el3.appTitle).to.equal('test1');
170
174
  expect(el3.appTitle2).to.equal('test2');
171
175
  expect(el3.renderCount).to.equal(0);
176
+ // 挂载前更新不会造成挂载后多余的渲染
177
+ el3.update();
178
+ el3.appTitle = 'test3';
172
179
  container.append(el3);
173
180
  expect(el3.renderCount).to.equal(1);
181
+ await Promise.resolve();
182
+ expect(el3.renderCount).to.equal(1);
174
183
  });
175
184
  });
176
185
 
@@ -343,6 +352,16 @@ describe('gem element 继承', () => {
343
352
  await Promise.resolve();
344
353
  expect(window.name).to.equal(`${name}21`);
345
354
  });
355
+ it('setAttribute 设置基类观察的 attribute', async () => {
356
+ const el: InheritGem1 = await fixture(html`<inherit-gem1></inherit-gem1>`);
357
+ el.setAttribute('app-title', 'c');
358
+ expect(el.appTitle).to.equal('c');
359
+ const el1: InheritGem = await fixture(html`<inherit-gem></inherit-gem>`);
360
+ el1.setAttribute('app-title2', 'd');
361
+ el1.setAttribute('app-title', 'e');
362
+ expect(el1.appTitle2).to.equal('d');
363
+ expect(el1.appTitle).to.equal('e');
364
+ });
346
365
  });
347
366
  @customElement('render-empty')
348
367
  @shadow()
@@ -114,6 +114,19 @@ describe('基本 gem element 测试', () => {
114
114
  expect(el.hasAttribute('attr')).to.equal(false);
115
115
  });
116
116
 
117
+ it('setAttribute/removeAttribute/toggleAttribute 设置观察的 attribute', async () => {
118
+ const el: GemDemo = await fixture(html`<gem-demo></gem-demo>`);
119
+ el.setAttribute('attr', 'a');
120
+ el.toggleAttribute('disabled');
121
+ expect(el.attr).to.equal('a');
122
+ expect(el.disabled).to.equal(true);
123
+ el.removeAttribute('attr');
124
+ expect(el.attr).to.equal('');
125
+ expect(el.hasAttribute('attr')).to.equal(false);
126
+ await Promise.resolve();
127
+ expect(el.shadowRoot?.textContent).to.equal('attr: , disabled: true, count: 0, prop: , state: ');
128
+ });
129
+
117
130
  it('读取 prop', async () => {
118
131
  const el: GemDemo = await fixture(html`<gem-demo .prop=${{ value: 'prop' }}></gem-demo>`);
119
132
  expect(el.prop).to.deep.equal({ value: 'prop' });
@@ -161,3 +174,23 @@ describe('基本 gem element 测试', () => {
161
174
  expect(el.renderCount).to.equal(2);
162
175
  });
163
176
  });
177
+
178
+ @customElement('gem-name-state')
179
+ class GemNameStateElement extends GemElement {
180
+ state = createState({ name: 'guest' });
181
+
182
+ render() {
183
+ return html`${this.state.name}`;
184
+ }
185
+ }
186
+
187
+ describe('state `name` 字段', () => {
188
+ it('create and update state', async () => {
189
+ const el: GemNameStateElement = await fixture(html`<gem-name-state></gem-name-state>`);
190
+ expect(el.textContent).to.equal('guest');
191
+ el.state({ name: 'admin' });
192
+ await Promise.resolve();
193
+ expect({ ...el.state }).to.deep.equal({ name: 'admin' });
194
+ expect(el.textContent).to.equal('admin');
195
+ });
196
+ });
@@ -58,6 +58,14 @@ describe('history 测试', () => {
58
58
  await aTimeout(10);
59
59
  expect(history.getParams().title).to.equal('321');
60
60
  });
61
+ it('keep title when only hash changed', async () => {
62
+ history.replace({ title: 'Page' });
63
+ // 模拟应用格式化 document.title
64
+ document.title = 'Page | Site';
65
+ location.hash = '#x';
66
+ await aTimeout(10);
67
+ expect(history.getParams().title).to.equal('Page');
68
+ });
61
69
  it('Native pushState/replaceState', async () => {
62
70
  const historyLength = window.history.length;
63
71
  window.history.pushState({}, 'title', '/a');
@@ -73,6 +81,17 @@ describe('history 测试', () => {
73
81
  expect(new URLSearchParams(location.search).get('tab')).to.equal('a');
74
82
  expect(window.history.length - historyLength).to.equal(1);
75
83
  });
84
+ it('Native pushState with hash', async () => {
85
+ let hashChangeCount = 0;
86
+ const onHashChange = () => hashChangeCount++;
87
+ addEventListener('hashchange', onHashChange);
88
+ window.history.pushState({}, '', '/c#a%20b');
89
+ removeEventListener('hashchange', onHashChange);
90
+ expect(window.location.pathname).to.equal('/c');
91
+ expect(window.location.hash).to.equal('#a%20b');
92
+ expect(history.getParams().hash).to.equal('#a b');
93
+ expect(hashChangeCount).to.equal(1);
94
+ });
76
95
  it('pushIgnoreCloseHandle', async () => {
77
96
  const { href } = window.location;
78
97
  let state = { open: true };
@@ -25,3 +25,15 @@ describe('store 测试', () => {
25
25
  expect(flag).to.equal(true);
26
26
  });
27
27
  });
28
+
29
+ describe('store `name` `length` 字段', () => {
30
+ it('create and update store', () => {
31
+ const store = createStore({ name: 'guest', length: 1 });
32
+ expect({ ...store }).to.deep.equal({ name: 'guest', length: 1 });
33
+ store({ name: 'admin', length: 2 });
34
+ expect({ ...store }).to.deep.equal({ name: 'admin', length: 2 });
35
+ const store2 = createStore<{ name?: string }>({});
36
+ store2({ name: 'guest' });
37
+ expect(store2.name).to.equal('guest');
38
+ });
39
+ });
@@ -1,4 +1,4 @@
1
- import { createTheme } from '../helper/theme';
1
+ import { createDecoratorTheme, createScopedTheme, createTheme } from '../helper/theme';
2
2
  import { aTimeout, expect } from './utils';
3
3
 
4
4
  describe('theme 测试', () => {
@@ -14,4 +14,11 @@ describe('theme 测试', () => {
14
14
  expect(getComputedStyle(el).width).to.equal('1px');
15
15
  el.remove();
16
16
  });
17
+
18
+ it('key 作为变量名后缀', () => {
19
+ expect(createTheme({ textColor: 'red' }, 'k1').textColor).to.equal('var(--text-color-k1)');
20
+ expect(createScopedTheme({ size: '1px' }, 'k2').size).to.equal('var(--size-k2)');
21
+ expect(createDecoratorTheme({ size: '1px' }, 'k3').size).to.equal('var(--size-k3)');
22
+ expect(createTheme({ size: '1px' }).size).to.not.equal(createTheme({ size: '1px' }).size, '没有 key 时随机');
23
+ });
17
24
  });
@@ -116,6 +116,12 @@ describe('utils 测试', () => {
116
116
  expect(bodyStyleSheet.cssRules.item(0).cssText.startsWith('@scope (body)')).to.true;
117
117
  expect(bodyStyleSheet).to.equal(bodyStyleSheet);
118
118
  expect(bodyStyleSheet).not.to.equal(cssSheet[SheetToken].getStyle(document.documentElement));
119
+ // 更新内容时保留 light DOM 的作用域
120
+ cssSheet[SheetToken].setContent('div { color: blue; }');
121
+ cssSheet[SheetToken].updateStyle();
122
+ expect(bodyStyleSheet.cssRules.item(0).cssText.startsWith('@scope (body)')).to.true;
123
+ expect(bodyStyleSheet.cssRules.item(0).cssText).to.include('blue');
124
+ expect(cssSheet[SheetToken].getStyle().cssRules.item(0).style.color).to.equal('blue');
119
125
  });
120
126
  it('raw/css', () => {
121
127
  const title: any = undefined;
package/src/test/utils.ts CHANGED
@@ -13,6 +13,62 @@ export function aTimeout(ms = 0) {
13
13
  return new Promise((resolve) => setTimeout(() => resolve(null), ms));
14
14
  }
15
15
 
16
+ export function createFakeTimers() {
17
+ type Task = { at: number; every?: number; fn: () => void };
18
+ const tasks = new Map<number, Task>();
19
+ const originals = {
20
+ setTimeout: window.setTimeout,
21
+ setInterval: window.setInterval,
22
+ clearTimeout: window.clearTimeout,
23
+ clearInterval: window.clearInterval,
24
+ };
25
+ const realClearTimeout = window.clearTimeout.bind(window);
26
+ let now = 0;
27
+ let nextId = -1;
28
+
29
+ const schedule = (fn: () => void, delay = 0, every?: number) => {
30
+ const id = nextId--;
31
+ tasks.set(id, { at: now + Math.max(delay, 0), every, fn });
32
+ return id;
33
+ };
34
+
35
+ return {
36
+ install: () => {
37
+ window.setTimeout = ((fn: () => void, delay?: number) =>
38
+ schedule(fn, delay)) as unknown as typeof window.setTimeout;
39
+ window.setInterval = ((fn: () => void, delay?: number) =>
40
+ schedule(fn, delay, Math.max(delay ?? 1, 1))) as unknown as typeof window.setInterval;
41
+ const clear = ((id: number) => {
42
+ if (!tasks.delete(id)) realClearTimeout(id);
43
+ }) as unknown as typeof window.clearTimeout;
44
+ window.clearTimeout = clear;
45
+ window.clearInterval = clear;
46
+ },
47
+ uninstall: () => {
48
+ window.setTimeout = originals.setTimeout;
49
+ window.setInterval = originals.setInterval;
50
+ window.clearTimeout = originals.clearTimeout;
51
+ window.clearInterval = originals.clearInterval;
52
+ tasks.clear();
53
+ },
54
+ tick: (ms: number) => {
55
+ const end = now + ms;
56
+ for (;;) {
57
+ let next: { id: number; task: Task } | undefined;
58
+ for (const [id, task] of tasks) {
59
+ if (task.at <= end && (!next || task.at < next.task.at)) next = { id, task };
60
+ }
61
+ if (!next) break;
62
+ now = next.task.at;
63
+ if (next.task.every === undefined) tasks.delete(next.id);
64
+ else next.task.at += next.task.every;
65
+ next.task.fn();
66
+ }
67
+ now = end;
68
+ },
69
+ };
70
+ }
71
+
16
72
  export async function fixture(template: TemplateResult) {
17
73
  const wrapper = document.createElement('div');
18
74
  document.body.appendChild(wrapper);