@furystack/shades-common-components 12.0.1 → 12.1.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.
- package/CHANGELOG.md +43 -0
- package/README.md +26 -0
- package/esm/components/cache-view.d.ts +46 -0
- package/esm/components/cache-view.d.ts.map +1 -0
- package/esm/components/cache-view.js +65 -0
- package/esm/components/cache-view.js.map +1 -0
- package/esm/components/cache-view.spec.d.ts +2 -0
- package/esm/components/cache-view.spec.d.ts.map +1 -0
- package/esm/components/cache-view.spec.js +183 -0
- package/esm/components/cache-view.spec.js.map +1 -0
- package/esm/components/index.d.ts +1 -0
- package/esm/components/index.d.ts.map +1 -1
- package/esm/components/index.js +1 -0
- package/esm/components/index.js.map +1 -1
- package/esm/components/skeleton.d.ts.map +1 -1
- package/esm/components/skeleton.js +2 -11
- package/esm/components/skeleton.js.map +1 -1
- package/esm/components/skeleton.spec.js +6 -55
- package/esm/components/skeleton.spec.js.map +1 -1
- package/package.json +2 -1
- package/src/components/cache-view.spec.tsx +210 -0
- package/src/components/cache-view.tsx +103 -0
- package/src/components/index.ts +1 -0
- package/src/components/skeleton.spec.tsx +6 -73
- package/src/components/skeleton.tsx +2 -11
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,48 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [12.1.0] - 2026-02-11
|
|
4
|
+
|
|
5
|
+
### ✨ Features
|
|
6
|
+
|
|
7
|
+
### New `CacheView` component
|
|
8
|
+
|
|
9
|
+
Added a new `CacheView` component that renders the state of a cache entry. It subscribes to a `Cache` instance observable and handles all states automatically:
|
|
10
|
+
|
|
11
|
+
1. **Error first** — shows error UI with a retry button
|
|
12
|
+
2. **Value next** — renders the content component (triggers reload when obsolete)
|
|
13
|
+
3. **Loading last** — shows a custom loader or nothing by default
|
|
14
|
+
|
|
15
|
+
```tsx
|
|
16
|
+
import { CacheView } from '@furystack/shades-common-components'
|
|
17
|
+
|
|
18
|
+
<CacheView cache={userCache} args={[userId]} content={UserContent} />
|
|
19
|
+
|
|
20
|
+
// With custom loader and error UI
|
|
21
|
+
<CacheView
|
|
22
|
+
cache={userCache}
|
|
23
|
+
args={[userId]}
|
|
24
|
+
content={UserContent}
|
|
25
|
+
loader={<Skeleton />}
|
|
26
|
+
error={(err, retry) => (
|
|
27
|
+
<Alert severity="error">
|
|
28
|
+
<Button onclick={retry}>Retry</Button>
|
|
29
|
+
</Alert>
|
|
30
|
+
)}
|
|
31
|
+
/>
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### 🐛 Bug Fixes
|
|
35
|
+
|
|
36
|
+
- Fixed `Skeleton` component background styles not rendering correctly when used inside Shadow DOM — moved gradient styles from host CSS to inline styles on the inner element
|
|
37
|
+
|
|
38
|
+
### 📚 Documentation
|
|
39
|
+
|
|
40
|
+
- Added `CacheView` usage examples to the package README
|
|
41
|
+
|
|
42
|
+
### ⬆️ Dependencies
|
|
43
|
+
|
|
44
|
+
- Added `@furystack/cache` (workspace:^) as a new dependency
|
|
45
|
+
|
|
3
46
|
## [12.0.1] - 2026-02-11
|
|
4
47
|
|
|
5
48
|
### 🧪 Tests
|
package/README.md
CHANGED
|
@@ -150,6 +150,32 @@ import { Tabs } from '@furystack/shades-common-components'
|
|
|
150
150
|
/>
|
|
151
151
|
```
|
|
152
152
|
|
|
153
|
+
### CacheView
|
|
154
|
+
|
|
155
|
+
Renders the state of a cache entry. Takes a `Cache` instance and `args`, subscribes to the observable, and handles loading, error (with retry), and loaded/obsolete states.
|
|
156
|
+
|
|
157
|
+
```tsx
|
|
158
|
+
import { CacheView } from '@furystack/shades-common-components'
|
|
159
|
+
import type { CacheWithValue } from '@furystack/cache'
|
|
160
|
+
|
|
161
|
+
const UserContent = Shade<{ data: CacheWithValue<User> }>({
|
|
162
|
+
shadowDomName: 'user-content',
|
|
163
|
+
render: ({ props }) => <div>{props.data.value.name}</div>,
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
// Basic usage
|
|
167
|
+
<CacheView cache={userCache} args={[userId]} content={UserContent} />
|
|
168
|
+
|
|
169
|
+
// With custom loader and error
|
|
170
|
+
<CacheView
|
|
171
|
+
cache={userCache}
|
|
172
|
+
args={[userId]}
|
|
173
|
+
content={UserContent}
|
|
174
|
+
loader={<Skeleton />}
|
|
175
|
+
error={(err, retry) => <Alert severity="error"><Button onclick={retry}>Retry</Button></Alert>}
|
|
176
|
+
/>
|
|
177
|
+
```
|
|
178
|
+
|
|
153
179
|
### Loader
|
|
154
180
|
|
|
155
181
|
A loading spinner component.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Cache, CacheWithValue } from '@furystack/cache';
|
|
2
|
+
import type { ShadeComponent } from '@furystack/shades';
|
|
3
|
+
/**
|
|
4
|
+
* Props for the CacheView component.
|
|
5
|
+
* @typeParam TData - The type of data stored in the cache
|
|
6
|
+
* @typeParam TArgs - The tuple type of arguments used to identify the cache entry
|
|
7
|
+
*/
|
|
8
|
+
export type CacheViewProps<TData, TArgs extends any[]> = {
|
|
9
|
+
/** The cache instance to observe and control */
|
|
10
|
+
cache: Cache<TData, TArgs>;
|
|
11
|
+
/** The arguments identifying which cache entry to display */
|
|
12
|
+
args: TArgs;
|
|
13
|
+
/** Shades component rendered when a value is available (loaded or obsolete). Receives CacheWithValue<TData>. */
|
|
14
|
+
content: ShadeComponent<{
|
|
15
|
+
data: CacheWithValue<TData>;
|
|
16
|
+
}>;
|
|
17
|
+
/** Optional custom loader element. Default: null (nothing shown when loading). */
|
|
18
|
+
loader?: JSX.Element;
|
|
19
|
+
/**
|
|
20
|
+
* Optional custom error UI. Receives the error and a retry callback.
|
|
21
|
+
* The retry callback calls cache.reload(...args).
|
|
22
|
+
* If not provided, a default Result + retry Button is shown.
|
|
23
|
+
*/
|
|
24
|
+
error?: (error: unknown, retry: () => void) => JSX.Element;
|
|
25
|
+
};
|
|
26
|
+
/**
|
|
27
|
+
* CacheView renders the state of a cache entry for the given cache + args.
|
|
28
|
+
*
|
|
29
|
+
* It subscribes to the cache observable and handles all states:
|
|
30
|
+
* 1. **Error first** - If the cache entry has failed, shows the error UI with a retry button.
|
|
31
|
+
* 2. **Value next** - If the entry has a value (loaded or obsolete), renders the content component.
|
|
32
|
+
* When obsolete, it also triggers a reload automatically.
|
|
33
|
+
* 3. **Loading last** - If there is no value and no error, shows the loader (or null by default).
|
|
34
|
+
*
|
|
35
|
+
* @example
|
|
36
|
+
* ```tsx
|
|
37
|
+
* const MyContent = Shade<{ data: CacheWithValue<User> }>({
|
|
38
|
+
* shadowDomName: 'my-content',
|
|
39
|
+
* render: ({ props }) => <div>{props.data.value.name}</div>,
|
|
40
|
+
* })
|
|
41
|
+
*
|
|
42
|
+
* <CacheView cache={userCache} args={[userId]} content={MyContent} />
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export declare const CacheView: <TData, TArgs extends any[]>(props: CacheViewProps<TData, TArgs>) => JSX.Element;
|
|
46
|
+
//# sourceMappingURL=cache-view.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-view.d.ts","sourceRoot":"","sources":["../../src/components/cache-view.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,KAAK,EAAE,cAAc,EAAE,MAAM,kBAAkB,CAAA;AAE7D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAA;AAMvD;;;;GAIG;AACH,MAAM,MAAM,cAAc,CAAC,KAAK,EAAE,KAAK,SAAS,GAAG,EAAE,IAAI;IACvD,gDAAgD;IAChD,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;IAC1B,6DAA6D;IAC7D,IAAI,EAAE,KAAK,CAAA;IACX,gHAAgH;IAChH,OAAO,EAAE,cAAc,CAAC;QAAE,IAAI,EAAE,cAAc,CAAC,KAAK,CAAC,CAAA;KAAE,CAAC,CAAA;IACxD,kFAAkF;IAClF,MAAM,CAAC,EAAE,GAAG,CAAC,OAAO,CAAA;IACpB;;;;OAIG;IACH,KAAK,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,EAAE,MAAM,IAAI,KAAK,GAAG,CAAC,OAAO,CAAA;CAC3D,CAAA;AAWD;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,SAAS,EAAE,CAAC,KAAK,EAAE,KAAK,SAAS,GAAG,EAAE,EAAE,KAAK,EAAE,cAAc,CAAC,KAAK,EAAE,KAAK,CAAC,KAAK,GAAG,CAAC,OA4C/F,CAAA"}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { hasCacheValue, isFailedCacheResult, isObsoleteCacheResult } from '@furystack/cache';
|
|
2
|
+
import { Shade, createComponent } from '@furystack/shades';
|
|
3
|
+
import { Button } from './button.js';
|
|
4
|
+
import { Result } from './result.js';
|
|
5
|
+
const getDefaultErrorUi = (error, retry) => (createComponent(Result, { status: "error", title: "Something went wrong", subtitle: String(error) },
|
|
6
|
+
createComponent(Button, { variant: "outlined", onclick: retry }, "Retry")));
|
|
7
|
+
/**
|
|
8
|
+
* CacheView renders the state of a cache entry for the given cache + args.
|
|
9
|
+
*
|
|
10
|
+
* It subscribes to the cache observable and handles all states:
|
|
11
|
+
* 1. **Error first** - If the cache entry has failed, shows the error UI with a retry button.
|
|
12
|
+
* 2. **Value next** - If the entry has a value (loaded or obsolete), renders the content component.
|
|
13
|
+
* When obsolete, it also triggers a reload automatically.
|
|
14
|
+
* 3. **Loading last** - If there is no value and no error, shows the loader (or null by default).
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```tsx
|
|
18
|
+
* const MyContent = Shade<{ data: CacheWithValue<User> }>({
|
|
19
|
+
* shadowDomName: 'my-content',
|
|
20
|
+
* render: ({ props }) => <div>{props.data.value.name}</div>,
|
|
21
|
+
* })
|
|
22
|
+
*
|
|
23
|
+
* <CacheView cache={userCache} args={[userId]} content={MyContent} />
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export const CacheView = Shade({
|
|
27
|
+
shadowDomName: 'shade-cache-view',
|
|
28
|
+
render: ({ props, useObservable, useState }) => {
|
|
29
|
+
const { cache, args, content, loader, error } = props;
|
|
30
|
+
const argsKey = JSON.stringify(args);
|
|
31
|
+
const observable = cache.getObservable(...args);
|
|
32
|
+
const [result] = useObservable(`cache-${argsKey}`, observable);
|
|
33
|
+
const [lastReloadedArgsKey, setLastReloadedArgsKey] = useState('lastReloadedArgsKey', null);
|
|
34
|
+
const retry = () => {
|
|
35
|
+
cache.reload(...args).catch(() => {
|
|
36
|
+
/* error state will be set by cache */
|
|
37
|
+
});
|
|
38
|
+
};
|
|
39
|
+
// 1. Error first
|
|
40
|
+
if (isFailedCacheResult(result)) {
|
|
41
|
+
const errorRenderer = error ?? getDefaultErrorUi;
|
|
42
|
+
return errorRenderer(result.error, retry);
|
|
43
|
+
}
|
|
44
|
+
// 2. Value next
|
|
45
|
+
if (hasCacheValue(result)) {
|
|
46
|
+
if (isObsoleteCacheResult(result)) {
|
|
47
|
+
if (lastReloadedArgsKey !== argsKey) {
|
|
48
|
+
setLastReloadedArgsKey(argsKey);
|
|
49
|
+
cache.reload(...args).catch(() => {
|
|
50
|
+
/* error state will be set by cache */
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
else if (lastReloadedArgsKey !== null) {
|
|
55
|
+
setLastReloadedArgsKey(null);
|
|
56
|
+
}
|
|
57
|
+
return createComponent(content, {
|
|
58
|
+
data: result,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
// 3. Loading last
|
|
62
|
+
return loader ?? null;
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
//# sourceMappingURL=cache-view.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-view.js","sourceRoot":"","sources":["../../src/components/cache-view.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,mBAAmB,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAA;AAE5F,OAAO,EAAE,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAE1D,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AACpC,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAA;AAwBpC,MAAM,iBAAiB,GAAG,CAAC,KAAc,EAAE,KAAiB,EAAe,EAAE,CAC3E,CACE,gBAAC,MAAM,IAAC,MAAM,EAAC,OAAO,EAAC,KAAK,EAAC,sBAAsB,EAAC,QAAQ,EAAE,MAAM,CAAC,KAAK,CAAC;IACzE,gBAAC,MAAM,IAAC,OAAO,EAAC,UAAU,EAAC,OAAO,EAAE,KAAK,YAEhC,CACF,CACgB,CAAA;AAE7B;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,MAAM,SAAS,GAAqF,KAAK,CAAC;IAC/G,aAAa,EAAE,kBAAkB;IACjC,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,aAAa,EAAE,QAAQ,EAAE,EAAsB,EAAE;QACjE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,KAAK,CAAA;QAErD,MAAM,OAAO,GAAG,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAA;QACpC,MAAM,UAAU,GAAG,KAAK,CAAC,aAAa,CAAC,GAAG,IAAI,CAAC,CAAA;QAE/C,MAAM,CAAC,MAAM,CAAC,GAAG,aAAa,CAAC,SAAS,OAAO,EAAE,EAAE,UAAU,CAAC,CAAA;QAE9D,MAAM,CAAC,mBAAmB,EAAE,sBAAsB,CAAC,GAAG,QAAQ,CAAgB,qBAAqB,EAAE,IAAI,CAAC,CAAA;QAE1G,MAAM,KAAK,GAAG,GAAG,EAAE;YACjB,KAAK,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE;gBAC/B,sCAAsC;YACxC,CAAC,CAAC,CAAA;QACJ,CAAC,CAAA;QAED,iBAAiB;QACjB,IAAI,mBAAmB,CAAC,MAAM,CAAC,EAAE,CAAC;YAChC,MAAM,aAAa,GAAG,KAAK,IAAI,iBAAiB,CAAA;YAChD,OAAO,aAAa,CAAC,MAAM,CAAC,KAAK,EAAE,KAAK,CAAC,CAAA;QAC3C,CAAC;QAED,gBAAgB;QAChB,IAAI,aAAa,CAAC,MAAM,CAAC,EAAE,CAAC;YAC1B,IAAI,qBAAqB,CAAC,MAAM,CAAC,EAAE,CAAC;gBAClC,IAAI,mBAAmB,KAAK,OAAO,EAAE,CAAC;oBACpC,sBAAsB,CAAC,OAAO,CAAC,CAAA;oBAC/B,KAAK,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE;wBAC/B,sCAAsC;oBACxC,CAAC,CAAC,CAAA;gBACJ,CAAC;YACH,CAAC;iBAAM,IAAI,mBAAmB,KAAK,IAAI,EAAE,CAAC;gBACxC,sBAAsB,CAAC,IAAI,CAAC,CAAA;YAC9B,CAAC;YACD,OAAO,eAAe,CAAC,OAA4D,EAAE;gBACnF,IAAI,EAAE,MAAM;aACb,CAA2B,CAAA;QAC9B,CAAC;QAED,kBAAkB;QAClB,OAAO,MAAM,IAAI,IAAI,CAAA;IACvB,CAAC;CACF,CAAC,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-view.spec.d.ts","sourceRoot":"","sources":["../../src/components/cache-view.spec.tsx"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { Cache } from '@furystack/cache';
|
|
2
|
+
import { Shade, createComponent, flushUpdates } from '@furystack/shades';
|
|
3
|
+
import { sleepAsync } from '@furystack/utils';
|
|
4
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { CacheView } from './cache-view.js';
|
|
6
|
+
const TestContent = Shade({
|
|
7
|
+
shadowDomName: 'test-cache-content',
|
|
8
|
+
render: ({ props }) => createComponent("span", { className: "content-value" }, props.data.value),
|
|
9
|
+
});
|
|
10
|
+
const renderCacheView = async (cache, args, options) => {
|
|
11
|
+
const el = (createComponent("div", null,
|
|
12
|
+
createComponent(CacheView, { cache: cache, args: args, content: TestContent, loader: options?.loader, error: options?.error })));
|
|
13
|
+
const cacheView = el.firstElementChild;
|
|
14
|
+
cacheView.updateComponent();
|
|
15
|
+
await flushUpdates();
|
|
16
|
+
return { container: el, cacheView };
|
|
17
|
+
};
|
|
18
|
+
describe('CacheView', () => {
|
|
19
|
+
it('should be defined', () => {
|
|
20
|
+
expect(CacheView).toBeDefined();
|
|
21
|
+
expect(typeof CacheView).toBe('function');
|
|
22
|
+
});
|
|
23
|
+
it('should create a cache-view element', () => {
|
|
24
|
+
const cache = new Cache({ load: async (key) => key });
|
|
25
|
+
const el = (createComponent(CacheView, { cache: cache, args: ['test'], content: TestContent }));
|
|
26
|
+
expect(el).toBeDefined();
|
|
27
|
+
expect(el.tagName?.toLowerCase()).toBe('shade-cache-view');
|
|
28
|
+
cache[Symbol.dispose]();
|
|
29
|
+
});
|
|
30
|
+
describe('loading state', () => {
|
|
31
|
+
it('should render null by default when loading', async () => {
|
|
32
|
+
const cache = new Cache({
|
|
33
|
+
load: () => new Promise(() => { }),
|
|
34
|
+
});
|
|
35
|
+
const { cacheView } = await renderCacheView(cache, ['test']);
|
|
36
|
+
expect(cacheView.querySelector('test-cache-content')).toBeNull();
|
|
37
|
+
expect(cacheView.querySelector('shade-result')).toBeNull();
|
|
38
|
+
cache[Symbol.dispose]();
|
|
39
|
+
});
|
|
40
|
+
it('should render custom loader when provided', async () => {
|
|
41
|
+
const cache = new Cache({
|
|
42
|
+
load: () => new Promise(() => { }),
|
|
43
|
+
});
|
|
44
|
+
const { cacheView } = await renderCacheView(cache, ['test'], {
|
|
45
|
+
loader: (createComponent("span", { className: "custom-loader" }, "Loading...")),
|
|
46
|
+
});
|
|
47
|
+
const loader = cacheView.querySelector('.custom-loader');
|
|
48
|
+
expect(loader).not.toBeNull();
|
|
49
|
+
expect(loader?.textContent).toBe('Loading...');
|
|
50
|
+
cache[Symbol.dispose]();
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
describe('loaded state', () => {
|
|
54
|
+
it('should render content when cache has loaded value', async () => {
|
|
55
|
+
const cache = new Cache({ load: async (key) => `Hello ${key}` });
|
|
56
|
+
await cache.get('world');
|
|
57
|
+
const { cacheView } = await renderCacheView(cache, ['world']);
|
|
58
|
+
const contentEl = cacheView.querySelector('test-cache-content');
|
|
59
|
+
expect(contentEl).not.toBeNull();
|
|
60
|
+
const contentComponent = contentEl;
|
|
61
|
+
contentComponent.updateComponent();
|
|
62
|
+
await flushUpdates();
|
|
63
|
+
const valueEl = contentComponent.querySelector('.content-value');
|
|
64
|
+
expect(valueEl?.textContent).toBe('Hello world');
|
|
65
|
+
cache[Symbol.dispose]();
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
describe('failed state', () => {
|
|
69
|
+
it('should render default error UI when cache has failed', async () => {
|
|
70
|
+
const cache = new Cache({
|
|
71
|
+
load: async () => {
|
|
72
|
+
throw new Error('Test failure');
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
try {
|
|
76
|
+
await cache.get('test');
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// expected
|
|
80
|
+
}
|
|
81
|
+
const { cacheView } = await renderCacheView(cache, ['test']);
|
|
82
|
+
const resultEl = cacheView.querySelector('shade-result');
|
|
83
|
+
expect(resultEl).not.toBeNull();
|
|
84
|
+
const resultComponent = resultEl;
|
|
85
|
+
resultComponent.updateComponent();
|
|
86
|
+
await flushUpdates();
|
|
87
|
+
expect(resultComponent.querySelector('.result-title')?.textContent).toBe('Something went wrong');
|
|
88
|
+
cache[Symbol.dispose]();
|
|
89
|
+
});
|
|
90
|
+
it('should render custom error UI when error prop is provided', async () => {
|
|
91
|
+
const cache = new Cache({
|
|
92
|
+
load: async () => {
|
|
93
|
+
throw new Error('Custom failure');
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
try {
|
|
97
|
+
await cache.get('test');
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
// expected
|
|
101
|
+
}
|
|
102
|
+
const customError = vi.fn((err, _retry) => (createComponent("span", { className: "custom-error" }, String(err))));
|
|
103
|
+
const { cacheView } = await renderCacheView(cache, ['test'], { error: customError });
|
|
104
|
+
expect(customError).toHaveBeenCalledOnce();
|
|
105
|
+
expect(customError.mock.calls[0][0]).toBeInstanceOf(Error);
|
|
106
|
+
const customErrorEl = cacheView.querySelector('.custom-error');
|
|
107
|
+
expect(customErrorEl).not.toBeNull();
|
|
108
|
+
cache[Symbol.dispose]();
|
|
109
|
+
});
|
|
110
|
+
it('should not render content when failed even if stale value exists', async () => {
|
|
111
|
+
const cache = new Cache({ load: async (key) => key });
|
|
112
|
+
await cache.get('test');
|
|
113
|
+
cache.setExplicitValue({
|
|
114
|
+
loadArgs: ['test'],
|
|
115
|
+
value: { status: 'failed', error: new Error('fail'), value: 'stale', updatedAt: new Date() },
|
|
116
|
+
});
|
|
117
|
+
const { cacheView } = await renderCacheView(cache, ['test']);
|
|
118
|
+
expect(cacheView.querySelector('test-cache-content')).toBeNull();
|
|
119
|
+
expect(cacheView.querySelector('shade-result')).not.toBeNull();
|
|
120
|
+
cache[Symbol.dispose]();
|
|
121
|
+
});
|
|
122
|
+
it('should call cache.reload when retry is invoked', async () => {
|
|
123
|
+
const loadFn = vi.fn(async () => {
|
|
124
|
+
throw new Error('fail');
|
|
125
|
+
});
|
|
126
|
+
const cache = new Cache({ load: loadFn });
|
|
127
|
+
try {
|
|
128
|
+
await cache.get('test');
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// expected
|
|
132
|
+
}
|
|
133
|
+
let capturedRetry;
|
|
134
|
+
const customError = (_err, retry) => {
|
|
135
|
+
capturedRetry = retry;
|
|
136
|
+
return (createComponent("span", { className: "custom-error" }, "Error"));
|
|
137
|
+
};
|
|
138
|
+
await renderCacheView(cache, ['test'], { error: customError });
|
|
139
|
+
expect(capturedRetry).toBeDefined();
|
|
140
|
+
loadFn.mockResolvedValueOnce('recovered');
|
|
141
|
+
capturedRetry();
|
|
142
|
+
await sleepAsync(50);
|
|
143
|
+
const observable = cache.getObservable('test');
|
|
144
|
+
const result = observable.getValue();
|
|
145
|
+
expect(result.status).toBe('loaded');
|
|
146
|
+
expect(result.value).toBe('recovered');
|
|
147
|
+
cache[Symbol.dispose]();
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
describe('obsolete state', () => {
|
|
151
|
+
it('should render content when obsolete and trigger reload', async () => {
|
|
152
|
+
const loadFn = vi.fn(async (key) => `Hello ${key}`);
|
|
153
|
+
const cache = new Cache({ load: loadFn });
|
|
154
|
+
await cache.get('test');
|
|
155
|
+
cache.setObsolete('test');
|
|
156
|
+
const { cacheView } = await renderCacheView(cache, ['test']);
|
|
157
|
+
const contentEl = cacheView.querySelector('test-cache-content');
|
|
158
|
+
expect(contentEl).not.toBeNull();
|
|
159
|
+
await sleepAsync(50);
|
|
160
|
+
// reload should have been called (initial load + obsolete reload)
|
|
161
|
+
expect(loadFn).toHaveBeenCalledTimes(2);
|
|
162
|
+
cache[Symbol.dispose]();
|
|
163
|
+
});
|
|
164
|
+
});
|
|
165
|
+
describe('error takes priority over value', () => {
|
|
166
|
+
it('should show error when failed with value, not content', async () => {
|
|
167
|
+
const cache = new Cache({ load: async (key) => key });
|
|
168
|
+
await cache.get('test');
|
|
169
|
+
const failedWithValue = {
|
|
170
|
+
status: 'failed',
|
|
171
|
+
error: new Error('whoops'),
|
|
172
|
+
value: 'stale-data',
|
|
173
|
+
updatedAt: new Date(),
|
|
174
|
+
};
|
|
175
|
+
cache.setExplicitValue({ loadArgs: ['test'], value: failedWithValue });
|
|
176
|
+
const { cacheView } = await renderCacheView(cache, ['test']);
|
|
177
|
+
expect(cacheView.querySelector('test-cache-content')).toBeNull();
|
|
178
|
+
expect(cacheView.querySelector('shade-result')).not.toBeNull();
|
|
179
|
+
cache[Symbol.dispose]();
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
});
|
|
183
|
+
//# sourceMappingURL=cache-view.spec.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache-view.spec.js","sourceRoot":"","sources":["../../src/components/cache-view.spec.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,kBAAkB,CAAA;AAExC,OAAO,EAAE,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAA;AACxE,OAAO,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AAC7C,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AACjD,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAE3C,MAAM,WAAW,GAAG,KAAK,CAAmC;IAC1D,aAAa,EAAE,oBAAoB;IACnC,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,EAAE,EAAE,CAAC,0BAAM,SAAS,EAAC,eAAe,IAAE,KAAK,CAAC,IAAI,CAAC,KAAK,CAAQ;CACjF,CAAC,CAAA;AAEF,MAAM,eAAe,GAAG,KAAK,EAC3B,KAA8B,EAC9B,IAAc,EACd,OAGC,EACD,EAAE;IACF,MAAM,EAAE,GAAG,CACT;QACE,gBAAC,SAAS,IAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,KAAK,GAAI,CACzG,CACP,CAAA;IACD,MAAM,SAAS,GAAG,EAAE,CAAC,iBAAgC,CAAA;IACrD,SAAS,CAAC,eAAe,EAAE,CAAA;IAC3B,MAAM,YAAY,EAAE,CAAA;IACpB,OAAO,EAAE,SAAS,EAAE,EAAE,EAAE,SAAS,EAAE,CAAA;AACrC,CAAC,CAAA;AAED,QAAQ,CAAC,WAAW,EAAE,GAAG,EAAE;IACzB,EAAE,CAAC,mBAAmB,EAAE,GAAG,EAAE;QAC3B,MAAM,CAAC,SAAS,CAAC,CAAC,WAAW,EAAE,CAAA;QAC/B,MAAM,CAAC,OAAO,SAAS,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,CAAA;IAC3C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,MAAM,KAAK,GAAG,IAAI,KAAK,CAAmB,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE,CAAC,GAAG,EAAE,CAAC,CAAA;QACvE,MAAM,EAAE,GAAG,CAAC,gBAAC,SAAS,IAAC,KAAK,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,MAAM,CAAC,EAAE,OAAO,EAAE,WAAW,GAAI,CAA2B,CAAA;QACxG,MAAM,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,CAAA;QACxB,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,WAAW,EAAE,CAAC,CAAC,IAAI,CAAC,kBAAkB,CAAC,CAAA;QAC1D,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAA;IACzB,CAAC,CAAC,CAAA;IAEF,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;QAC7B,EAAE,CAAC,4CAA4C,EAAE,KAAK,IAAI,EAAE;YAC1D,MAAM,KAAK,GAAG,IAAI,KAAK,CAAmB;gBACxC,IAAI,EAAE,GAAG,EAAE,CAAC,IAAI,OAAO,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC;aAClC,CAAC,CAAA;YACF,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,eAAe,CAAC,KAAK,EAAE,CAAC,MAAM,CAAC,CAAC,CAAA;YAC5D,MAAM,CAAC,SAAS,CAAC,aAAa,CAAC,oBAAoB,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAA;YAChE,MAAM,CAAC,SAAS,CAAC,aAAa,CAAC,cAAc,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAA;YAC1D,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAA;QACzB,CAAC,CAAC,CAAA;QAEF,EAAE,CAAC,2CAA2C,EAAE,KAAK,IAAI,EAAE;YACzD,MAAM,KAAK,GAAG,IAAI,KAAK,CAAmB;gBACxC,IAAI,EAAE,GAAG,EAAE,CAAC,IAAI,OAAO,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC;aAClC,CAAC,CAAA;YACF,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,eAAe,CAAC,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE;gBAC3D,MAAM,EAAE,CAAC,0BAAM,SAAS,EAAC,eAAe,iBAAkB,CAA2B;aACtF,CAAC,CAAA;YACF,MAAM,MAAM,GAAG,SAAS,CAAC,aAAa,CAAC,gBAAgB,CAAC,CAAA;YACxD,MAAM,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAA;YAC7B,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAA;YAC9C,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAA;QACzB,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,QAAQ,CAAC,cAAc,EAAE,GAAG,EAAE;QAC5B,EAAE,CAAC,mDAAmD,EAAE,KAAK,IAAI,EAAE;YACjE,MAAM,KAAK,GAAG,IAAI,KAAK,CAAmB,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE,CAAC,SAAS,GAAG,EAAE,EAAE,CAAC,CAAA;YAClF,MAAM,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAA;YACxB,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,eAAe,CAAC,KAAK,EAAE,CAAC,OAAO,CAAC,CAAC,CAAA;YAC7D,MAAM,SAAS,GAAG,SAAS,CAAC,aAAa,CAAC,oBAAoB,CAAC,CAAA;YAC/D,MAAM,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAA;YAChC,MAAM,gBAAgB,GAAG,SAAwB,CAAA;YACjD,gBAAgB,CAAC,eAAe,EAAE,CAAA;YAClC,MAAM,YAAY,EAAE,CAAA;YACpB,MAAM,OAAO,GAAG,gBAAgB,CAAC,aAAa,CAAC,gBAAgB,CAAC,CAAA;YAChE,MAAM,CAAC,OAAO,EAAE,WAAW,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAA;YAChD,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAA;QACzB,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,QAAQ,CAAC,cAAc,EAAE,GAAG,EAAE;QAC5B,EAAE,CAAC,sDAAsD,EAAE,KAAK,IAAI,EAAE;YACpE,MAAM,KAAK,GAAG,IAAI,KAAK,CAAmB;gBACxC,IAAI,EAAE,KAAK,IAAI,EAAE;oBACf,MAAM,IAAI,KAAK,CAAC,cAAc,CAAC,CAAA;gBACjC,CAAC;aACF,CAAC,CAAA;YACF,IAAI,CAAC;gBACH,MAAM,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;YACzB,CAAC;YAAC,MAAM,CAAC;gBACP,WAAW;YACb,CAAC;YACD,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,eAAe,CAAC,KAAK,EAAE,CAAC,MAAM,CAAC,CAAC,CAAA;YAC5D,MAAM,QAAQ,GAAG,SAAS,CAAC,aAAa,CAAC,cAAc,CAAC,CAAA;YACxD,MAAM,CAAC,QAAQ,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAA;YAC/B,MAAM,eAAe,GAAG,QAAuB,CAAA;YAC/C,eAAe,CAAC,eAAe,EAAE,CAAA;YACjC,MAAM,YAAY,EAAE,CAAA;YACpB,MAAM,CAAC,eAAe,CAAC,aAAa,CAAC,eAAe,CAAC,EAAE,WAAW,CAAC,CAAC,IAAI,CAAC,sBAAsB,CAAC,CAAA;YAChG,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAA;QACzB,CAAC,CAAC,CAAA;QAEF,EAAE,CAAC,2DAA2D,EAAE,KAAK,IAAI,EAAE;YACzE,MAAM,KAAK,GAAG,IAAI,KAAK,CAAmB;gBACxC,IAAI,EAAE,KAAK,IAAI,EAAE;oBACf,MAAM,IAAI,KAAK,CAAC,gBAAgB,CAAC,CAAA;gBACnC,CAAC;aACF,CAAC,CAAA;YACF,IAAI,CAAC;gBACH,MAAM,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;YACzB,CAAC;YAAC,MAAM,CAAC;gBACP,WAAW;YACb,CAAC;YACD,MAAM,WAAW,GAAG,EAAE,CAAC,EAAE,CACvB,CAAC,GAAY,EAAE,MAAkB,EAAE,EAAE,CACnC,CAAC,0BAAM,SAAS,EAAC,cAAc,IAAE,MAAM,CAAC,GAAG,CAAC,CAAQ,CAA2B,CAClF,CAAA;YACD,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,eAAe,CAAC,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,CAAA;YACpF,MAAM,CAAC,WAAW,CAAC,CAAC,oBAAoB,EAAE,CAAA;YAC1C,MAAM,CAAC,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,cAAc,CAAC,KAAK,CAAC,CAAA;YAC1D,MAAM,aAAa,GAAG,SAAS,CAAC,aAAa,CAAC,eAAe,CAAC,CAAA;YAC9D,MAAM,CAAC,aAAa,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAA;YACpC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAA;QACzB,CAAC,CAAC,CAAA;QAEF,EAAE,CAAC,kEAAkE,EAAE,KAAK,IAAI,EAAE;YAChF,MAAM,KAAK,GAAG,IAAI,KAAK,CAAmB,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE,CAAC,GAAG,EAAE,CAAC,CAAA;YACvE,MAAM,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;YACvB,KAAK,CAAC,gBAAgB,CAAC;gBACrB,QAAQ,EAAE,CAAC,MAAM,CAAC;gBAClB,KAAK,EAAE,EAAE,MAAM,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,KAAK,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,OAAO,EAAE,SAAS,EAAE,IAAI,IAAI,EAAE,EAAE;aAC7F,CAAC,CAAA;YACF,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,eAAe,CAAC,KAAK,EAAE,CAAC,MAAM,CAAC,CAAC,CAAA;YAC5D,MAAM,CAAC,SAAS,CAAC,aAAa,CAAC,oBAAoB,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAA;YAChE,MAAM,CAAC,SAAS,CAAC,aAAa,CAAC,cAAc,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAA;YAC9D,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAA;QACzB,CAAC,CAAC,CAAA;QAEF,EAAE,CAAC,gDAAgD,EAAE,KAAK,IAAI,EAAE;YAC9D,MAAM,MAAM,GAAG,EAAE,CAAC,EAAE,CAAmC,KAAK,IAAI,EAAE;gBAChE,MAAM,IAAI,KAAK,CAAC,MAAM,CAAC,CAAA;YACzB,CAAC,CAAC,CAAA;YACF,MAAM,KAAK,GAAG,IAAI,KAAK,CAAmB,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAA;YAC3D,IAAI,CAAC;gBACH,MAAM,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;YACzB,CAAC;YAAC,MAAM,CAAC;gBACP,WAAW;YACb,CAAC;YACD,IAAI,aAAuC,CAAA;YAC3C,MAAM,WAAW,GAAG,CAAC,IAAa,EAAE,KAAiB,EAAE,EAAE;gBACvD,aAAa,GAAG,KAAK,CAAA;gBACrB,OAAO,CAAC,0BAAM,SAAS,EAAC,cAAc,YAAa,CAA2B,CAAA;YAChF,CAAC,CAAA;YACD,MAAM,eAAe,CAAC,KAAK,EAAE,CAAC,MAAM,CAAC,EAAE,EAAE,KAAK,EAAE,WAAW,EAAE,CAAC,CAAA;YAC9D,MAAM,CAAC,aAAa,CAAC,CAAC,WAAW,EAAE,CAAA;YAEnC,MAAM,CAAC,qBAAqB,CAAC,WAAW,CAAC,CAAA;YACzC,aAAc,EAAE,CAAA;YAChB,MAAM,UAAU,CAAC,EAAE,CAAC,CAAA;YAEpB,MAAM,UAAU,GAAG,KAAK,CAAC,aAAa,CAAC,MAAM,CAAC,CAAA;YAC9C,MAAM,MAAM,GAAG,UAAU,CAAC,QAAQ,EAAE,CAAA;YACpC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;YACpC,MAAM,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,WAAW,CAAC,CAAA;YACtC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAA;QACzB,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;QAC9B,EAAE,CAAC,wDAAwD,EAAE,KAAK,IAAI,EAAE;YACtE,MAAM,MAAM,GAAG,EAAE,CAAC,EAAE,CAAC,KAAK,EAAE,GAAW,EAAE,EAAE,CAAC,SAAS,GAAG,EAAE,CAAC,CAAA;YAC3D,MAAM,KAAK,GAAG,IAAI,KAAK,CAAmB,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAA;YAC3D,MAAM,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;YACvB,KAAK,CAAC,WAAW,CAAC,MAAM,CAAC,CAAA;YAEzB,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,eAAe,CAAC,KAAK,EAAE,CAAC,MAAM,CAAC,CAAC,CAAA;YAC5D,MAAM,SAAS,GAAG,SAAS,CAAC,aAAa,CAAC,oBAAoB,CAAC,CAAA;YAC/D,MAAM,CAAC,SAAS,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAA;YAEhC,MAAM,UAAU,CAAC,EAAE,CAAC,CAAA;YACpB,kEAAkE;YAClE,MAAM,CAAC,MAAM,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,CAAA;YACvC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAA;QACzB,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;IAEF,QAAQ,CAAC,iCAAiC,EAAE,GAAG,EAAE;QAC/C,EAAE,CAAC,uDAAuD,EAAE,KAAK,IAAI,EAAE;YACrE,MAAM,KAAK,GAAG,IAAI,KAAK,CAAmB,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE,CAAC,GAAG,EAAE,CAAC,CAAA;YACvE,MAAM,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAA;YACvB,MAAM,eAAe,GAAwB;gBAC3C,MAAM,EAAE,QAAQ;gBAChB,KAAK,EAAE,IAAI,KAAK,CAAC,QAAQ,CAAC;gBAC1B,KAAK,EAAE,YAAY;gBACnB,SAAS,EAAE,IAAI,IAAI,EAAE;aACtB,CAAA;YACD,KAAK,CAAC,gBAAgB,CAAC,EAAE,QAAQ,EAAE,CAAC,MAAM,CAAC,EAAE,KAAK,EAAE,eAAe,EAAE,CAAC,CAAA;YACtE,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,eAAe,CAAC,KAAK,EAAE,CAAC,MAAM,CAAC,CAAC,CAAA;YAC5D,MAAM,CAAC,SAAS,CAAC,aAAa,CAAC,oBAAoB,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAA;YAChE,MAAM,CAAC,SAAS,CAAC,aAAa,CAAC,cAAc,CAAC,CAAC,CAAC,GAAG,CAAC,QAAQ,EAAE,CAAA;YAC9D,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,EAAE,CAAA;QACzB,CAAC,CAAC,CAAA;IACJ,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAA;AACpC,cAAc,YAAY,CAAA;AAC1B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,mBAAmB,CAAA;AACjC,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA;AAC1B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,mBAAmB,CAAA;AACjC,cAAc,aAAa,CAAA;AAC3B,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,WAAW,CAAA;AACzB,cAAc,wBAAwB,CAAA;AACtC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,yBAAyB,CAAA;AACvC,cAAc,sBAAsB,CAAA;AACpC,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,mBAAmB,CAAA;AACjC,cAAc,eAAe,CAAA;AAC7B,cAAc,UAAU,CAAA;AACxB,cAAc,WAAW,CAAA;AACzB,cAAc,WAAW,CAAA;AACzB,cAAc,kBAAkB,CAAA;AAChC,cAAc,YAAY,CAAA;AAC1B,cAAc,mBAAmB,CAAA;AACjC,cAAc,sBAAsB,CAAA;AACpC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,2BAA2B,CAAA;AACzC,cAAc,wBAAwB,CAAA;AACtC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,aAAa,CAAA;AAC3B,cAAc,oBAAoB,CAAA;AAClC,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,cAAc,CAAA;AAC5B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,mBAAmB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAA;AACpC,cAAc,YAAY,CAAA;AAC1B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,mBAAmB,CAAA;AACjC,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA;AAC1B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,mBAAmB,CAAA;AACjC,cAAc,aAAa,CAAA;AAC3B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,WAAW,CAAA;AACzB,cAAc,wBAAwB,CAAA;AACtC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,yBAAyB,CAAA;AACvC,cAAc,sBAAsB,CAAA;AACpC,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,mBAAmB,CAAA;AACjC,cAAc,eAAe,CAAA;AAC7B,cAAc,UAAU,CAAA;AACxB,cAAc,WAAW,CAAA;AACzB,cAAc,WAAW,CAAA;AACzB,cAAc,kBAAkB,CAAA;AAChC,cAAc,YAAY,CAAA;AAC1B,cAAc,mBAAmB,CAAA;AACjC,cAAc,sBAAsB,CAAA;AACpC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,2BAA2B,CAAA;AACzC,cAAc,wBAAwB,CAAA;AACtC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,aAAa,CAAA;AAC3B,cAAc,oBAAoB,CAAA;AAClC,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,cAAc,CAAA;AAC5B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,mBAAmB,CAAA"}
|
package/esm/components/index.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAA;AACpC,cAAc,YAAY,CAAA;AAC1B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,mBAAmB,CAAA;AACjC,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA;AAC1B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,mBAAmB,CAAA;AACjC,cAAc,aAAa,CAAA;AAC3B,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,WAAW,CAAA;AACzB,cAAc,wBAAwB,CAAA;AACtC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,yBAAyB,CAAA;AACvC,cAAc,sBAAsB,CAAA;AACpC,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,mBAAmB,CAAA;AACjC,cAAc,eAAe,CAAA;AAC7B,cAAc,UAAU,CAAA;AACxB,cAAc,WAAW,CAAA;AACzB,cAAc,WAAW,CAAA;AACzB,cAAc,kBAAkB,CAAA;AAChC,cAAc,YAAY,CAAA;AAC1B,cAAc,mBAAmB,CAAA;AACjC,cAAc,sBAAsB,CAAA;AACpC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,2BAA2B,CAAA;AACzC,cAAc,wBAAwB,CAAA;AACtC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,aAAa,CAAA;AAC3B,cAAc,oBAAoB,CAAA;AAClC,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,cAAc,CAAA;AAC5B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,mBAAmB,CAAA"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAA;AACpC,cAAc,YAAY,CAAA;AAC1B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,mBAAmB,CAAA;AACjC,cAAc,cAAc,CAAA;AAC5B,cAAc,aAAa,CAAA;AAC3B,cAAc,YAAY,CAAA;AAC1B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,mBAAmB,CAAA;AACjC,cAAc,aAAa,CAAA;AAC3B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,WAAW,CAAA;AACzB,cAAc,wBAAwB,CAAA;AACtC,cAAc,4BAA4B,CAAA;AAC1C,cAAc,yBAAyB,CAAA;AACvC,cAAc,sBAAsB,CAAA;AACpC,cAAc,aAAa,CAAA;AAC3B,cAAc,cAAc,CAAA;AAC5B,cAAc,mBAAmB,CAAA;AACjC,cAAc,eAAe,CAAA;AAC7B,cAAc,UAAU,CAAA;AACxB,cAAc,WAAW,CAAA;AACzB,cAAc,WAAW,CAAA;AACzB,cAAc,kBAAkB,CAAA;AAChC,cAAc,YAAY,CAAA;AAC1B,cAAc,mBAAmB,CAAA;AACjC,cAAc,sBAAsB,CAAA;AACpC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,YAAY,CAAA;AAC1B,cAAc,gBAAgB,CAAA;AAC9B,cAAc,2BAA2B,CAAA;AACzC,cAAc,wBAAwB,CAAA;AACtC,cAAc,iBAAiB,CAAA;AAC/B,cAAc,YAAY,CAAA;AAC1B,cAAc,aAAa,CAAA;AAC3B,cAAc,aAAa,CAAA;AAC3B,cAAc,eAAe,CAAA;AAC7B,cAAc,aAAa,CAAA;AAC3B,cAAc,oBAAoB,CAAA;AAClC,cAAc,WAAW,CAAA;AACzB,cAAc,eAAe,CAAA;AAC7B,cAAc,cAAc,CAAA;AAC5B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,mBAAmB,CAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skeleton.d.ts","sourceRoot":"","sources":["../../src/components/skeleton.tsx"],"names":[],"mappings":"AAIA,MAAM,MAAM,aAAa,GAAG;IAC1B;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAED,eAAO,MAAM,QAAQ;;;;
|
|
1
|
+
{"version":3,"file":"skeleton.d.ts","sourceRoot":"","sources":["../../src/components/skeleton.tsx"],"names":[],"mappings":"AAIA,MAAM,MAAM,aAAa,GAAG;IAC1B;;OAEG;IACH,KAAK,CAAC,EAAE,MAAM,CAAA;CACf,CAAA;AAED,eAAO,MAAM,QAAQ;;;;sEAwCnB,CAAA"}
|
|
@@ -3,15 +3,6 @@ import { cssVariableTheme } from '../services/css-variable-theme.js';
|
|
|
3
3
|
import { promisifyAnimation } from '../utils/promisify-animation.js';
|
|
4
4
|
export const Skeleton = Shade({
|
|
5
5
|
shadowDomName: 'shade-skeleton',
|
|
6
|
-
css: {
|
|
7
|
-
opacity: '0',
|
|
8
|
-
display: 'inline-block',
|
|
9
|
-
background: `linear-gradient(-45deg, color-mix(in srgb, ${cssVariableTheme.text.secondary} 10%, transparent), color-mix(in srgb, ${cssVariableTheme.text.secondary} 30%, transparent), color-mix(in srgb, ${cssVariableTheme.text.secondary} 10%, transparent))`,
|
|
10
|
-
backgroundSize: '400% 400%',
|
|
11
|
-
width: '100%',
|
|
12
|
-
height: '100%',
|
|
13
|
-
minHeight: '1em',
|
|
14
|
-
},
|
|
15
6
|
render: ({ props, useRef }) => {
|
|
16
7
|
const wrapperRef = useRef('wrapper');
|
|
17
8
|
const { delay = 1500 } = props;
|
|
@@ -34,8 +25,8 @@ export const Skeleton = Shade({
|
|
|
34
25
|
return (createComponent("div", { ref: wrapperRef, style: {
|
|
35
26
|
opacity: '0',
|
|
36
27
|
display: 'inline-block',
|
|
37
|
-
background:
|
|
38
|
-
backgroundSize: '
|
|
28
|
+
background: `linear-gradient(-45deg, color-mix(in srgb, ${cssVariableTheme.text.secondary} 10%, transparent), color-mix(in srgb, ${cssVariableTheme.text.secondary} 30%, transparent), color-mix(in srgb, ${cssVariableTheme.text.secondary} 10%, transparent))`,
|
|
29
|
+
backgroundSize: '400% 400%',
|
|
39
30
|
width: '100%',
|
|
40
31
|
height: '100%',
|
|
41
32
|
minHeight: '1em',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skeleton.js","sourceRoot":"","sources":["../../src/components/skeleton.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAC1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAA;AACpE,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAA;AASpE,MAAM,CAAC,MAAM,QAAQ,GAAG,KAAK,CAAgB;IAC3C,aAAa,EAAE,gBAAgB;IAC/B,
|
|
1
|
+
{"version":3,"file":"skeleton.js","sourceRoot":"","sources":["../../src/components/skeleton.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,eAAe,EAAE,MAAM,mBAAmB,CAAA;AAC1D,OAAO,EAAE,gBAAgB,EAAE,MAAM,mCAAmC,CAAA;AACpE,OAAO,EAAE,kBAAkB,EAAE,MAAM,iCAAiC,CAAA;AASpE,MAAM,CAAC,MAAM,QAAQ,GAAG,KAAK,CAAgB;IAC3C,aAAa,EAAE,gBAAgB;IAC/B,MAAM,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,EAAE;QAC5B,MAAM,UAAU,GAAG,MAAM,CAAiB,SAAS,CAAC,CAAA;QACpD,MAAM,EAAE,KAAK,GAAG,IAAI,EAAE,GAAG,KAAK,CAAA;QAC9B,UAAU,CAAC,GAAG,EAAE;YACd,MAAM,EAAE,GAAG,UAAU,CAAC,OAAO,CAAA;YAC7B,IAAI,CAAC,EAAE;gBAAE,OAAM;YACf,KAAK,kBAAkB,CAAC,EAAE,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,EAAE,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,EAAE;gBAC5D,IAAI,EAAE,UAAU;gBAChB,QAAQ,EAAE,GAAG;gBACb,MAAM,EAAE,UAAU;gBAClB,KAAK;aACN,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE;gBACX,KAAK,kBAAkB,CACrB,EAAE,EACF,CAAC,EAAE,kBAAkB,EAAE,QAAQ,EAAE,EAAE,EAAE,kBAAkB,EAAE,UAAU,EAAE,EAAE,EAAE,kBAAkB,EAAE,QAAQ,EAAE,CAAC,EACxG;oBACE,QAAQ,EAAE,KAAK;oBACf,UAAU,EAAE,QAAQ;iBACrB,CACF,CAAA;YACH,CAAC,CAAC,CAAA;QACJ,CAAC,CAAC,CAAA;QAEF,OAAO,CACL,yBACE,GAAG,EAAE,UAAU,EACf,KAAK,EAAE;gBACL,OAAO,EAAE,GAAG;gBACZ,OAAO,EAAE,cAAc;gBACvB,UAAU,EAAE,8CAA8C,gBAAgB,CAAC,IAAI,CAAC,SAAS,0CAA0C,gBAAgB,CAAC,IAAI,CAAC,SAAS,0CAA0C,gBAAgB,CAAC,IAAI,CAAC,SAAS,qBAAqB;gBAChQ,cAAc,EAAE,WAAW;gBAC3B,KAAK,EAAE,MAAM;gBACb,MAAM,EAAE,MAAM;gBACd,SAAS,EAAE,KAAK;aACjB,GACD,CACH,CAAA;IACH,CAAC;CACF,CAAC,CAAA"}
|
|
@@ -37,7 +37,7 @@ describe('Skeleton', () => {
|
|
|
37
37
|
Element.prototype.animate = originalAnimate;
|
|
38
38
|
vi.restoreAllMocks();
|
|
39
39
|
});
|
|
40
|
-
it('should render with
|
|
40
|
+
it('should render with correct initial state and default delay', async () => {
|
|
41
41
|
await usingAsync(new Injector(), async (injector) => {
|
|
42
42
|
const rootElement = document.getElementById('root');
|
|
43
43
|
initializeShadeRoot({
|
|
@@ -48,32 +48,11 @@ describe('Skeleton', () => {
|
|
|
48
48
|
await sleepAsync(50);
|
|
49
49
|
const skeleton = document.querySelector('shade-skeleton');
|
|
50
50
|
expect(skeleton).not.toBeNull();
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
await usingAsync(new Injector(), async (injector) => {
|
|
55
|
-
const rootElement = document.getElementById('root');
|
|
56
|
-
initializeShadeRoot({
|
|
57
|
-
injector,
|
|
58
|
-
rootElement,
|
|
59
|
-
jsxElement: createComponent(Skeleton, null),
|
|
60
|
-
});
|
|
61
|
-
await sleepAsync(50);
|
|
62
|
-
const skeleton = document.querySelector('shade-skeleton');
|
|
63
|
-
expect(skeleton).not.toBeNull();
|
|
64
|
-
const computedStyle = window.getComputedStyle(skeleton);
|
|
51
|
+
const skeletonDiv = document.querySelector('shade-skeleton div');
|
|
52
|
+
expect(skeletonDiv).not.toBeNull();
|
|
53
|
+
const computedStyle = window.getComputedStyle(skeletonDiv);
|
|
65
54
|
expect(computedStyle.opacity).toBe('0');
|
|
66
|
-
|
|
67
|
-
});
|
|
68
|
-
it('should use default delay of 1500ms', async () => {
|
|
69
|
-
await usingAsync(new Injector(), async (injector) => {
|
|
70
|
-
const rootElement = document.getElementById('root');
|
|
71
|
-
initializeShadeRoot({
|
|
72
|
-
injector,
|
|
73
|
-
rootElement,
|
|
74
|
-
jsxElement: createComponent(Skeleton, null),
|
|
75
|
-
});
|
|
76
|
-
await sleepAsync(50);
|
|
55
|
+
expect(computedStyle.display).toBe('inline-block');
|
|
77
56
|
const fadeInCall = animateCalls.find((call) => Array.isArray(call.keyframes) && call.keyframes.some((kf) => 'opacity' in kf));
|
|
78
57
|
expect(fadeInCall).toBeDefined();
|
|
79
58
|
expect(fadeInCall?.options?.delay).toBe(1500);
|
|
@@ -93,21 +72,6 @@ describe('Skeleton', () => {
|
|
|
93
72
|
expect(fadeInCall?.options?.delay).toBe(500);
|
|
94
73
|
});
|
|
95
74
|
});
|
|
96
|
-
it('should have correct css styles applied', async () => {
|
|
97
|
-
await usingAsync(new Injector(), async (injector) => {
|
|
98
|
-
const rootElement = document.getElementById('root');
|
|
99
|
-
initializeShadeRoot({
|
|
100
|
-
injector,
|
|
101
|
-
rootElement,
|
|
102
|
-
jsxElement: createComponent(Skeleton, null),
|
|
103
|
-
});
|
|
104
|
-
await sleepAsync(50);
|
|
105
|
-
const skeleton = document.querySelector('shade-skeleton');
|
|
106
|
-
expect(skeleton).not.toBeNull();
|
|
107
|
-
const computedStyle = window.getComputedStyle(skeleton);
|
|
108
|
-
expect(computedStyle.display).toBe('inline-block');
|
|
109
|
-
});
|
|
110
|
-
});
|
|
111
75
|
it('should start fade-in animation with correct parameters', async () => {
|
|
112
76
|
await usingAsync(new Injector(), async (injector) => {
|
|
113
77
|
const rootElement = document.getElementById('root');
|
|
@@ -129,7 +93,7 @@ describe('Skeleton', () => {
|
|
|
129
93
|
expect(options.delay).toBe(100);
|
|
130
94
|
});
|
|
131
95
|
});
|
|
132
|
-
it('should start background animation after fade-in completes', async () => {
|
|
96
|
+
it('should start background animation with correct keyframes after fade-in completes', async () => {
|
|
133
97
|
await usingAsync(new Injector(), async (injector) => {
|
|
134
98
|
const rootElement = document.getElementById('root');
|
|
135
99
|
initializeShadeRoot({
|
|
@@ -143,19 +107,6 @@ describe('Skeleton', () => {
|
|
|
143
107
|
const options = backgroundAnimation?.options;
|
|
144
108
|
expect(options.duration).toBe(10000);
|
|
145
109
|
expect(options.iterations).toBe(Infinity);
|
|
146
|
-
});
|
|
147
|
-
});
|
|
148
|
-
it('should have gradient background animation keyframes', async () => {
|
|
149
|
-
await usingAsync(new Injector(), async (injector) => {
|
|
150
|
-
const rootElement = document.getElementById('root');
|
|
151
|
-
initializeShadeRoot({
|
|
152
|
-
injector,
|
|
153
|
-
rootElement,
|
|
154
|
-
jsxElement: createComponent(Skeleton, { delay: 0 }),
|
|
155
|
-
});
|
|
156
|
-
await sleepAsync(100);
|
|
157
|
-
const backgroundAnimation = animateCalls.find((call) => Array.isArray(call.keyframes) && call.keyframes.some((kf) => 'backgroundPosition' in kf));
|
|
158
|
-
expect(backgroundAnimation).toBeDefined();
|
|
159
110
|
const keyframes = backgroundAnimation?.keyframes;
|
|
160
111
|
expect(keyframes).toHaveLength(3);
|
|
161
112
|
expect(keyframes[0].backgroundPosition).toBe('0% 50%');
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skeleton.spec.js","sourceRoot":"","sources":["../../src/components/skeleton.spec.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,mBAAmB,CAAA;AAC5C,OAAO,EAAE,eAAe,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAA;AACxE,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAA;AACzD,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AACxE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AAExC,QAAQ,CAAC,UAAU,EAAE,GAAG,EAAE;IACxB,IAAI,eAAiD,CAAA;IACrD,IAAI,YAA6D,CAAA;IAEjE,UAAU,CAAC,GAAG,EAAE;QACd,QAAQ,CAAC,IAAI,CAAC,SAAS,GAAG,uBAAuB,CAAA;QACjD,YAAY,GAAG,EAAE,CAAA;QACjB,eAAe,GAAG,OAAO,CAAC,SAAS,CAAC,OAAO,CAAA;QAE3C,OAAO,CAAC,SAAS,CAAC,OAAO,GAAG,EAAE,CAAC,EAAE,CAC/B,CAAC,SAAuD,EAAE,OAA2C,EAAE,EAAE;YACvG,YAAY,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,CAAA;YACzC,MAAM,aAAa,GAAG;gBACpB,QAAQ,EAAE,IAAwD;gBAClE,QAAQ,EAAE,IAAwD;gBAClE,MAAM,EAAE,EAAE,CAAC,EAAE,EAAE;gBACf,IAAI,EAAE,EAAE,CAAC,EAAE,EAAE;gBACb,KAAK,EAAE,EAAE,CAAC,EAAE,EAAE;gBACd,MAAM,EAAE,EAAE,CAAC,EAAE,EAAE;gBACf,gBAAgB,EAAE,EAAE,CAAC,EAAE,EAAE;gBACzB,mBAAmB,EAAE,EAAE,CAAC,EAAE,EAAE;aAC7B,CAAA;YAED,IAAI,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC,IAAI,SAAS,CAAC,IAAI,CAAC,CAAC,EAAY,EAAE,EAAE,CAAC,SAAS,IAAI,EAAE,CAAC,EAAE,CAAC;gBAClF,UAAU,CAAC,GAAG,EAAE;oBACd,IAAI,aAAa,CAAC,QAAQ,EAAE,CAAC;wBAC3B,aAAa,CAAC,QAAQ,CAAC,EAA4B,CAAC,CAAA;oBACtD,CAAC;gBACH,CAAC,EAAE,EAAE,CAAC,CAAA;YACR,CAAC;YAED,OAAO,aAAqC,CAAA;QAC9C,CAAC,CACkC,CAAA;IACvC,CAAC,CAAC,CAAA;IAEF,SAAS,CAAC,GAAG,EAAE;QACb,QAAQ,CAAC,IAAI,CAAC,SAAS,GAAG,EAAE,CAAA;QAC5B,OAAO,CAAC,SAAS,CAAC,OAAO,GAAG,eAAe,CAAA;QAC3C,EAAE,CAAC,eAAe,EAAE,CAAA;IACtB,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC
|
|
1
|
+
{"version":3,"file":"skeleton.spec.js","sourceRoot":"","sources":["../../src/components/skeleton.spec.tsx"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@furystack/shades-common-components",
|
|
3
|
-
"version": "12.0
|
|
3
|
+
"version": "12.1.0",
|
|
4
4
|
"description": "Common UI components for FuryStack Shades",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
@@ -44,6 +44,7 @@
|
|
|
44
44
|
"vitest": "^4.0.18"
|
|
45
45
|
},
|
|
46
46
|
"dependencies": {
|
|
47
|
+
"@furystack/cache": "^6.0.0",
|
|
47
48
|
"@furystack/core": "^15.0.36",
|
|
48
49
|
"@furystack/inject": "^12.0.30",
|
|
49
50
|
"@furystack/shades": "^12.0.1",
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
import { Cache } from '@furystack/cache'
|
|
2
|
+
import type { CacheResult, CacheWithValue } from '@furystack/cache'
|
|
3
|
+
import { Shade, createComponent, flushUpdates } from '@furystack/shades'
|
|
4
|
+
import { sleepAsync } from '@furystack/utils'
|
|
5
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
6
|
+
import { CacheView } from './cache-view.js'
|
|
7
|
+
|
|
8
|
+
const TestContent = Shade<{ data: CacheWithValue<string> }>({
|
|
9
|
+
shadowDomName: 'test-cache-content',
|
|
10
|
+
render: ({ props }) => <span className="content-value">{props.data.value}</span>,
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
const renderCacheView = async (
|
|
14
|
+
cache: Cache<string, [string]>,
|
|
15
|
+
args: [string],
|
|
16
|
+
options?: {
|
|
17
|
+
loader?: JSX.Element
|
|
18
|
+
error?: (error: unknown, retry: () => void) => JSX.Element
|
|
19
|
+
},
|
|
20
|
+
) => {
|
|
21
|
+
const el = (
|
|
22
|
+
<div>
|
|
23
|
+
<CacheView cache={cache} args={args} content={TestContent} loader={options?.loader} error={options?.error} />
|
|
24
|
+
</div>
|
|
25
|
+
)
|
|
26
|
+
const cacheView = el.firstElementChild as JSX.Element
|
|
27
|
+
cacheView.updateComponent()
|
|
28
|
+
await flushUpdates()
|
|
29
|
+
return { container: el, cacheView }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
describe('CacheView', () => {
|
|
33
|
+
it('should be defined', () => {
|
|
34
|
+
expect(CacheView).toBeDefined()
|
|
35
|
+
expect(typeof CacheView).toBe('function')
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('should create a cache-view element', () => {
|
|
39
|
+
const cache = new Cache<string, [string]>({ load: async (key) => key })
|
|
40
|
+
const el = (<CacheView cache={cache} args={['test']} content={TestContent} />) as unknown as HTMLElement
|
|
41
|
+
expect(el).toBeDefined()
|
|
42
|
+
expect(el.tagName?.toLowerCase()).toBe('shade-cache-view')
|
|
43
|
+
cache[Symbol.dispose]()
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
describe('loading state', () => {
|
|
47
|
+
it('should render null by default when loading', async () => {
|
|
48
|
+
const cache = new Cache<string, [string]>({
|
|
49
|
+
load: () => new Promise(() => {}),
|
|
50
|
+
})
|
|
51
|
+
const { cacheView } = await renderCacheView(cache, ['test'])
|
|
52
|
+
expect(cacheView.querySelector('test-cache-content')).toBeNull()
|
|
53
|
+
expect(cacheView.querySelector('shade-result')).toBeNull()
|
|
54
|
+
cache[Symbol.dispose]()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('should render custom loader when provided', async () => {
|
|
58
|
+
const cache = new Cache<string, [string]>({
|
|
59
|
+
load: () => new Promise(() => {}),
|
|
60
|
+
})
|
|
61
|
+
const { cacheView } = await renderCacheView(cache, ['test'], {
|
|
62
|
+
loader: (<span className="custom-loader">Loading...</span>) as unknown as JSX.Element,
|
|
63
|
+
})
|
|
64
|
+
const loader = cacheView.querySelector('.custom-loader')
|
|
65
|
+
expect(loader).not.toBeNull()
|
|
66
|
+
expect(loader?.textContent).toBe('Loading...')
|
|
67
|
+
cache[Symbol.dispose]()
|
|
68
|
+
})
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
describe('loaded state', () => {
|
|
72
|
+
it('should render content when cache has loaded value', async () => {
|
|
73
|
+
const cache = new Cache<string, [string]>({ load: async (key) => `Hello ${key}` })
|
|
74
|
+
await cache.get('world')
|
|
75
|
+
const { cacheView } = await renderCacheView(cache, ['world'])
|
|
76
|
+
const contentEl = cacheView.querySelector('test-cache-content')
|
|
77
|
+
expect(contentEl).not.toBeNull()
|
|
78
|
+
const contentComponent = contentEl as JSX.Element
|
|
79
|
+
contentComponent.updateComponent()
|
|
80
|
+
await flushUpdates()
|
|
81
|
+
const valueEl = contentComponent.querySelector('.content-value')
|
|
82
|
+
expect(valueEl?.textContent).toBe('Hello world')
|
|
83
|
+
cache[Symbol.dispose]()
|
|
84
|
+
})
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
describe('failed state', () => {
|
|
88
|
+
it('should render default error UI when cache has failed', async () => {
|
|
89
|
+
const cache = new Cache<string, [string]>({
|
|
90
|
+
load: async () => {
|
|
91
|
+
throw new Error('Test failure')
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
try {
|
|
95
|
+
await cache.get('test')
|
|
96
|
+
} catch {
|
|
97
|
+
// expected
|
|
98
|
+
}
|
|
99
|
+
const { cacheView } = await renderCacheView(cache, ['test'])
|
|
100
|
+
const resultEl = cacheView.querySelector('shade-result')
|
|
101
|
+
expect(resultEl).not.toBeNull()
|
|
102
|
+
const resultComponent = resultEl as JSX.Element
|
|
103
|
+
resultComponent.updateComponent()
|
|
104
|
+
await flushUpdates()
|
|
105
|
+
expect(resultComponent.querySelector('.result-title')?.textContent).toBe('Something went wrong')
|
|
106
|
+
cache[Symbol.dispose]()
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
it('should render custom error UI when error prop is provided', async () => {
|
|
110
|
+
const cache = new Cache<string, [string]>({
|
|
111
|
+
load: async () => {
|
|
112
|
+
throw new Error('Custom failure')
|
|
113
|
+
},
|
|
114
|
+
})
|
|
115
|
+
try {
|
|
116
|
+
await cache.get('test')
|
|
117
|
+
} catch {
|
|
118
|
+
// expected
|
|
119
|
+
}
|
|
120
|
+
const customError = vi.fn(
|
|
121
|
+
(err: unknown, _retry: () => void) =>
|
|
122
|
+
(<span className="custom-error">{String(err)}</span>) as unknown as JSX.Element,
|
|
123
|
+
)
|
|
124
|
+
const { cacheView } = await renderCacheView(cache, ['test'], { error: customError })
|
|
125
|
+
expect(customError).toHaveBeenCalledOnce()
|
|
126
|
+
expect(customError.mock.calls[0][0]).toBeInstanceOf(Error)
|
|
127
|
+
const customErrorEl = cacheView.querySelector('.custom-error')
|
|
128
|
+
expect(customErrorEl).not.toBeNull()
|
|
129
|
+
cache[Symbol.dispose]()
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
it('should not render content when failed even if stale value exists', async () => {
|
|
133
|
+
const cache = new Cache<string, [string]>({ load: async (key) => key })
|
|
134
|
+
await cache.get('test')
|
|
135
|
+
cache.setExplicitValue({
|
|
136
|
+
loadArgs: ['test'],
|
|
137
|
+
value: { status: 'failed', error: new Error('fail'), value: 'stale', updatedAt: new Date() },
|
|
138
|
+
})
|
|
139
|
+
const { cacheView } = await renderCacheView(cache, ['test'])
|
|
140
|
+
expect(cacheView.querySelector('test-cache-content')).toBeNull()
|
|
141
|
+
expect(cacheView.querySelector('shade-result')).not.toBeNull()
|
|
142
|
+
cache[Symbol.dispose]()
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('should call cache.reload when retry is invoked', async () => {
|
|
146
|
+
const loadFn = vi.fn<(key: string) => Promise<string>>(async () => {
|
|
147
|
+
throw new Error('fail')
|
|
148
|
+
})
|
|
149
|
+
const cache = new Cache<string, [string]>({ load: loadFn })
|
|
150
|
+
try {
|
|
151
|
+
await cache.get('test')
|
|
152
|
+
} catch {
|
|
153
|
+
// expected
|
|
154
|
+
}
|
|
155
|
+
let capturedRetry: (() => void) | undefined
|
|
156
|
+
const customError = (_err: unknown, retry: () => void) => {
|
|
157
|
+
capturedRetry = retry
|
|
158
|
+
return (<span className="custom-error">Error</span>) as unknown as JSX.Element
|
|
159
|
+
}
|
|
160
|
+
await renderCacheView(cache, ['test'], { error: customError })
|
|
161
|
+
expect(capturedRetry).toBeDefined()
|
|
162
|
+
|
|
163
|
+
loadFn.mockResolvedValueOnce('recovered')
|
|
164
|
+
capturedRetry!()
|
|
165
|
+
await sleepAsync(50)
|
|
166
|
+
|
|
167
|
+
const observable = cache.getObservable('test')
|
|
168
|
+
const result = observable.getValue()
|
|
169
|
+
expect(result.status).toBe('loaded')
|
|
170
|
+
expect(result.value).toBe('recovered')
|
|
171
|
+
cache[Symbol.dispose]()
|
|
172
|
+
})
|
|
173
|
+
})
|
|
174
|
+
|
|
175
|
+
describe('obsolete state', () => {
|
|
176
|
+
it('should render content when obsolete and trigger reload', async () => {
|
|
177
|
+
const loadFn = vi.fn(async (key: string) => `Hello ${key}`)
|
|
178
|
+
const cache = new Cache<string, [string]>({ load: loadFn })
|
|
179
|
+
await cache.get('test')
|
|
180
|
+
cache.setObsolete('test')
|
|
181
|
+
|
|
182
|
+
const { cacheView } = await renderCacheView(cache, ['test'])
|
|
183
|
+
const contentEl = cacheView.querySelector('test-cache-content')
|
|
184
|
+
expect(contentEl).not.toBeNull()
|
|
185
|
+
|
|
186
|
+
await sleepAsync(50)
|
|
187
|
+
// reload should have been called (initial load + obsolete reload)
|
|
188
|
+
expect(loadFn).toHaveBeenCalledTimes(2)
|
|
189
|
+
cache[Symbol.dispose]()
|
|
190
|
+
})
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
describe('error takes priority over value', () => {
|
|
194
|
+
it('should show error when failed with value, not content', async () => {
|
|
195
|
+
const cache = new Cache<string, [string]>({ load: async (key) => key })
|
|
196
|
+
await cache.get('test')
|
|
197
|
+
const failedWithValue: CacheResult<string> = {
|
|
198
|
+
status: 'failed',
|
|
199
|
+
error: new Error('whoops'),
|
|
200
|
+
value: 'stale-data',
|
|
201
|
+
updatedAt: new Date(),
|
|
202
|
+
}
|
|
203
|
+
cache.setExplicitValue({ loadArgs: ['test'], value: failedWithValue })
|
|
204
|
+
const { cacheView } = await renderCacheView(cache, ['test'])
|
|
205
|
+
expect(cacheView.querySelector('test-cache-content')).toBeNull()
|
|
206
|
+
expect(cacheView.querySelector('shade-result')).not.toBeNull()
|
|
207
|
+
cache[Symbol.dispose]()
|
|
208
|
+
})
|
|
209
|
+
})
|
|
210
|
+
})
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import type { Cache, CacheWithValue } from '@furystack/cache'
|
|
2
|
+
import { hasCacheValue, isFailedCacheResult, isObsoleteCacheResult } from '@furystack/cache'
|
|
3
|
+
import type { ShadeComponent } from '@furystack/shades'
|
|
4
|
+
import { Shade, createComponent } from '@furystack/shades'
|
|
5
|
+
|
|
6
|
+
import { Button } from './button.js'
|
|
7
|
+
import { Result } from './result.js'
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Props for the CacheView component.
|
|
11
|
+
* @typeParam TData - The type of data stored in the cache
|
|
12
|
+
* @typeParam TArgs - The tuple type of arguments used to identify the cache entry
|
|
13
|
+
*/
|
|
14
|
+
export type CacheViewProps<TData, TArgs extends any[]> = {
|
|
15
|
+
/** The cache instance to observe and control */
|
|
16
|
+
cache: Cache<TData, TArgs>
|
|
17
|
+
/** The arguments identifying which cache entry to display */
|
|
18
|
+
args: TArgs
|
|
19
|
+
/** Shades component rendered when a value is available (loaded or obsolete). Receives CacheWithValue<TData>. */
|
|
20
|
+
content: ShadeComponent<{ data: CacheWithValue<TData> }>
|
|
21
|
+
/** Optional custom loader element. Default: null (nothing shown when loading). */
|
|
22
|
+
loader?: JSX.Element
|
|
23
|
+
/**
|
|
24
|
+
* Optional custom error UI. Receives the error and a retry callback.
|
|
25
|
+
* The retry callback calls cache.reload(...args).
|
|
26
|
+
* If not provided, a default Result + retry Button is shown.
|
|
27
|
+
*/
|
|
28
|
+
error?: (error: unknown, retry: () => void) => JSX.Element
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const getDefaultErrorUi = (error: unknown, retry: () => void): JSX.Element =>
|
|
32
|
+
(
|
|
33
|
+
<Result status="error" title="Something went wrong" subtitle={String(error)}>
|
|
34
|
+
<Button variant="outlined" onclick={retry}>
|
|
35
|
+
Retry
|
|
36
|
+
</Button>
|
|
37
|
+
</Result>
|
|
38
|
+
) as unknown as JSX.Element
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* CacheView renders the state of a cache entry for the given cache + args.
|
|
42
|
+
*
|
|
43
|
+
* It subscribes to the cache observable and handles all states:
|
|
44
|
+
* 1. **Error first** - If the cache entry has failed, shows the error UI with a retry button.
|
|
45
|
+
* 2. **Value next** - If the entry has a value (loaded or obsolete), renders the content component.
|
|
46
|
+
* When obsolete, it also triggers a reload automatically.
|
|
47
|
+
* 3. **Loading last** - If there is no value and no error, shows the loader (or null by default).
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* ```tsx
|
|
51
|
+
* const MyContent = Shade<{ data: CacheWithValue<User> }>({
|
|
52
|
+
* shadowDomName: 'my-content',
|
|
53
|
+
* render: ({ props }) => <div>{props.data.value.name}</div>,
|
|
54
|
+
* })
|
|
55
|
+
*
|
|
56
|
+
* <CacheView cache={userCache} args={[userId]} content={MyContent} />
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export const CacheView: <TData, TArgs extends any[]>(props: CacheViewProps<TData, TArgs>) => JSX.Element = Shade({
|
|
60
|
+
shadowDomName: 'shade-cache-view',
|
|
61
|
+
render: ({ props, useObservable, useState }): JSX.Element | null => {
|
|
62
|
+
const { cache, args, content, loader, error } = props
|
|
63
|
+
|
|
64
|
+
const argsKey = JSON.stringify(args)
|
|
65
|
+
const observable = cache.getObservable(...args)
|
|
66
|
+
|
|
67
|
+
const [result] = useObservable(`cache-${argsKey}`, observable)
|
|
68
|
+
|
|
69
|
+
const [lastReloadedArgsKey, setLastReloadedArgsKey] = useState<string | null>('lastReloadedArgsKey', null)
|
|
70
|
+
|
|
71
|
+
const retry = () => {
|
|
72
|
+
cache.reload(...args).catch(() => {
|
|
73
|
+
/* error state will be set by cache */
|
|
74
|
+
})
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 1. Error first
|
|
78
|
+
if (isFailedCacheResult(result)) {
|
|
79
|
+
const errorRenderer = error ?? getDefaultErrorUi
|
|
80
|
+
return errorRenderer(result.error, retry)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 2. Value next
|
|
84
|
+
if (hasCacheValue(result)) {
|
|
85
|
+
if (isObsoleteCacheResult(result)) {
|
|
86
|
+
if (lastReloadedArgsKey !== argsKey) {
|
|
87
|
+
setLastReloadedArgsKey(argsKey)
|
|
88
|
+
cache.reload(...args).catch(() => {
|
|
89
|
+
/* error state will be set by cache */
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
} else if (lastReloadedArgsKey !== null) {
|
|
93
|
+
setLastReloadedArgsKey(null)
|
|
94
|
+
}
|
|
95
|
+
return createComponent(content as ShadeComponent<{ data: CacheWithValue<unknown> }>, {
|
|
96
|
+
data: result,
|
|
97
|
+
}) as unknown as JSX.Element
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 3. Loading last
|
|
101
|
+
return loader ?? null
|
|
102
|
+
},
|
|
103
|
+
})
|
package/src/components/index.ts
CHANGED
|
@@ -46,7 +46,7 @@ describe('Skeleton', () => {
|
|
|
46
46
|
vi.restoreAllMocks()
|
|
47
47
|
})
|
|
48
48
|
|
|
49
|
-
it('should render with
|
|
49
|
+
it('should render with correct initial state and default delay', async () => {
|
|
50
50
|
await usingAsync(new Injector(), async (injector) => {
|
|
51
51
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
52
52
|
|
|
@@ -60,40 +60,13 @@ describe('Skeleton', () => {
|
|
|
60
60
|
|
|
61
61
|
const skeleton = document.querySelector('shade-skeleton')
|
|
62
62
|
expect(skeleton).not.toBeNull()
|
|
63
|
-
})
|
|
64
|
-
})
|
|
65
|
-
|
|
66
|
-
it('should have initial opacity of 0', async () => {
|
|
67
|
-
await usingAsync(new Injector(), async (injector) => {
|
|
68
|
-
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
69
|
-
|
|
70
|
-
initializeShadeRoot({
|
|
71
|
-
injector,
|
|
72
|
-
rootElement,
|
|
73
|
-
jsxElement: <Skeleton />,
|
|
74
|
-
})
|
|
75
|
-
|
|
76
|
-
await sleepAsync(50)
|
|
77
63
|
|
|
78
|
-
const
|
|
79
|
-
expect(
|
|
64
|
+
const skeletonDiv = document.querySelector('shade-skeleton div') as HTMLElement
|
|
65
|
+
expect(skeletonDiv).not.toBeNull()
|
|
80
66
|
|
|
81
|
-
const computedStyle = window.getComputedStyle(
|
|
67
|
+
const computedStyle = window.getComputedStyle(skeletonDiv)
|
|
82
68
|
expect(computedStyle.opacity).toBe('0')
|
|
83
|
-
|
|
84
|
-
})
|
|
85
|
-
|
|
86
|
-
it('should use default delay of 1500ms', async () => {
|
|
87
|
-
await usingAsync(new Injector(), async (injector) => {
|
|
88
|
-
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
89
|
-
|
|
90
|
-
initializeShadeRoot({
|
|
91
|
-
injector,
|
|
92
|
-
rootElement,
|
|
93
|
-
jsxElement: <Skeleton />,
|
|
94
|
-
})
|
|
95
|
-
|
|
96
|
-
await sleepAsync(50)
|
|
69
|
+
expect(computedStyle.display).toBe('inline-block')
|
|
97
70
|
|
|
98
71
|
const fadeInCall = animateCalls.find(
|
|
99
72
|
(call) => Array.isArray(call.keyframes) && call.keyframes.some((kf: Keyframe) => 'opacity' in kf),
|
|
@@ -125,26 +98,6 @@ describe('Skeleton', () => {
|
|
|
125
98
|
})
|
|
126
99
|
})
|
|
127
100
|
|
|
128
|
-
it('should have correct css styles applied', async () => {
|
|
129
|
-
await usingAsync(new Injector(), async (injector) => {
|
|
130
|
-
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
131
|
-
|
|
132
|
-
initializeShadeRoot({
|
|
133
|
-
injector,
|
|
134
|
-
rootElement,
|
|
135
|
-
jsxElement: <Skeleton />,
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
await sleepAsync(50)
|
|
139
|
-
|
|
140
|
-
const skeleton = document.querySelector('shade-skeleton') as HTMLElement
|
|
141
|
-
expect(skeleton).not.toBeNull()
|
|
142
|
-
|
|
143
|
-
const computedStyle = window.getComputedStyle(skeleton)
|
|
144
|
-
expect(computedStyle.display).toBe('inline-block')
|
|
145
|
-
})
|
|
146
|
-
})
|
|
147
|
-
|
|
148
101
|
it('should start fade-in animation with correct parameters', async () => {
|
|
149
102
|
await usingAsync(new Injector(), async (injector) => {
|
|
150
103
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
@@ -175,7 +128,7 @@ describe('Skeleton', () => {
|
|
|
175
128
|
})
|
|
176
129
|
})
|
|
177
130
|
|
|
178
|
-
it('should start background animation after fade-in completes', async () => {
|
|
131
|
+
it('should start background animation with correct keyframes after fade-in completes', async () => {
|
|
179
132
|
await usingAsync(new Injector(), async (injector) => {
|
|
180
133
|
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
181
134
|
|
|
@@ -196,26 +149,6 @@ describe('Skeleton', () => {
|
|
|
196
149
|
const options = backgroundAnimation?.options as KeyframeAnimationOptions
|
|
197
150
|
expect(options.duration).toBe(10000)
|
|
198
151
|
expect(options.iterations).toBe(Infinity)
|
|
199
|
-
})
|
|
200
|
-
})
|
|
201
|
-
|
|
202
|
-
it('should have gradient background animation keyframes', async () => {
|
|
203
|
-
await usingAsync(new Injector(), async (injector) => {
|
|
204
|
-
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
205
|
-
|
|
206
|
-
initializeShadeRoot({
|
|
207
|
-
injector,
|
|
208
|
-
rootElement,
|
|
209
|
-
jsxElement: <Skeleton delay={0} />,
|
|
210
|
-
})
|
|
211
|
-
|
|
212
|
-
await sleepAsync(100)
|
|
213
|
-
|
|
214
|
-
const backgroundAnimation = animateCalls.find(
|
|
215
|
-
(call) => Array.isArray(call.keyframes) && call.keyframes.some((kf: Keyframe) => 'backgroundPosition' in kf),
|
|
216
|
-
)
|
|
217
|
-
|
|
218
|
-
expect(backgroundAnimation).toBeDefined()
|
|
219
152
|
|
|
220
153
|
const keyframes = backgroundAnimation?.keyframes as Keyframe[]
|
|
221
154
|
expect(keyframes).toHaveLength(3)
|
|
@@ -11,15 +11,6 @@ export type SkeletonProps = {
|
|
|
11
11
|
|
|
12
12
|
export const Skeleton = Shade<SkeletonProps>({
|
|
13
13
|
shadowDomName: 'shade-skeleton',
|
|
14
|
-
css: {
|
|
15
|
-
opacity: '0',
|
|
16
|
-
display: 'inline-block',
|
|
17
|
-
background: `linear-gradient(-45deg, color-mix(in srgb, ${cssVariableTheme.text.secondary} 10%, transparent), color-mix(in srgb, ${cssVariableTheme.text.secondary} 30%, transparent), color-mix(in srgb, ${cssVariableTheme.text.secondary} 10%, transparent))`,
|
|
18
|
-
backgroundSize: '400% 400%',
|
|
19
|
-
width: '100%',
|
|
20
|
-
height: '100%',
|
|
21
|
-
minHeight: '1em',
|
|
22
|
-
},
|
|
23
14
|
render: ({ props, useRef }) => {
|
|
24
15
|
const wrapperRef = useRef<HTMLDivElement>('wrapper')
|
|
25
16
|
const { delay = 1500 } = props
|
|
@@ -49,8 +40,8 @@ export const Skeleton = Shade<SkeletonProps>({
|
|
|
49
40
|
style={{
|
|
50
41
|
opacity: '0',
|
|
51
42
|
display: 'inline-block',
|
|
52
|
-
background:
|
|
53
|
-
backgroundSize: '
|
|
43
|
+
background: `linear-gradient(-45deg, color-mix(in srgb, ${cssVariableTheme.text.secondary} 10%, transparent), color-mix(in srgb, ${cssVariableTheme.text.secondary} 30%, transparent), color-mix(in srgb, ${cssVariableTheme.text.secondary} 10%, transparent))`,
|
|
44
|
+
backgroundSize: '400% 400%',
|
|
54
45
|
width: '100%',
|
|
55
46
|
height: '100%',
|
|
56
47
|
minHeight: '1em',
|