@furystack/shades 3.7.3 → 4.0.4
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/dist/component-factory.spec.js +68 -11
- package/dist/component-factory.spec.js.map +1 -1
- package/dist/components/lazy-load.spec.d.ts +2 -0
- package/dist/components/lazy-load.spec.d.ts.map +1 -0
- package/dist/components/lazy-load.spec.js +83 -0
- package/dist/components/lazy-load.spec.js.map +1 -0
- package/dist/components/route-link.d.ts.map +1 -1
- package/dist/components/route-link.js.map +1 -1
- package/dist/components/route-link.spec.d.ts +2 -0
- package/dist/components/route-link.spec.d.ts.map +1 -0
- package/dist/components/route-link.spec.js +38 -0
- package/dist/components/route-link.spec.js.map +1 -0
- package/dist/components/router.d.ts +4 -2
- package/dist/components/router.d.ts.map +1 -1
- package/dist/components/router.js +35 -26
- package/dist/components/router.js.map +1 -1
- package/dist/components/router.spec.d.ts +2 -0
- package/dist/components/router.spec.d.ts.map +1 -0
- package/dist/components/router.spec.js +93 -0
- package/dist/components/router.spec.js.map +1 -0
- package/dist/services/location-service.d.ts +1 -1
- package/dist/services/location-service.d.ts.map +1 -1
- package/dist/services/location-service.js +3 -2
- package/dist/services/location-service.js.map +1 -1
- package/dist/services/location-service.spec.js +29 -3
- package/dist/services/location-service.spec.js.map +1 -1
- package/dist/services/screen-service.d.ts +1 -1
- package/dist/services/screen-service.d.ts.map +1 -1
- package/dist/services/screen-service.js +5 -5
- package/dist/services/screen-service.js.map +1 -1
- package/dist/services/screen-service.spec.d.ts +2 -0
- package/dist/services/screen-service.spec.d.ts.map +1 -0
- package/dist/services/screen-service.spec.js +34 -0
- package/dist/services/screen-service.spec.js.map +1 -0
- package/dist/shade-resources.integration.spec.d.ts +2 -0
- package/dist/shade-resources.integration.spec.d.ts.map +1 -0
- package/dist/shade-resources.integration.spec.js +62 -0
- package/dist/shade-resources.integration.spec.js.map +1 -0
- package/dist/shade.d.ts +2 -0
- package/dist/shade.d.ts.map +1 -1
- package/dist/shade.js +22 -6
- package/dist/shade.js.map +1 -1
- package/dist/shades.integration.spec.d.ts +2 -0
- package/dist/shades.integration.spec.d.ts.map +1 -0
- package/dist/shades.integration.spec.js +123 -0
- package/dist/shades.integration.spec.js.map +1 -0
- package/package.json +8 -5
- package/src/component-factory.spec.tsx +97 -14
- package/src/components/lazy-load.spec.tsx +117 -0
- package/src/components/route-link.spec.tsx +50 -0
- package/src/components/route-link.tsx +2 -1
- package/src/components/router.spec.tsx +130 -0
- package/src/components/router.tsx +38 -29
- package/src/services/location-service.spec.ts +37 -3
- package/src/services/location-service.tsx +3 -2
- package/src/services/screen-service.spec.ts +39 -0
- package/src/services/screen-service.ts +1 -1
- package/src/shade-resources.integration.spec.tsx +94 -0
- package/src/shade.ts +29 -9
- package/src/shades.integration.spec.tsx +156 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { TextEncoder, TextDecoder } from 'util'
|
|
2
|
+
|
|
3
|
+
global.TextEncoder = TextEncoder
|
|
4
|
+
global.TextDecoder = TextDecoder as any
|
|
5
|
+
|
|
6
|
+
import { Injector } from '@furystack/inject'
|
|
7
|
+
import { sleepAsync } from '@furystack/utils'
|
|
8
|
+
import { LazyLoad } from './lazy-load'
|
|
9
|
+
import { JSDOM } from 'jsdom'
|
|
10
|
+
import { createComponent, initializeShadeRoot } from '..'
|
|
11
|
+
|
|
12
|
+
describe('Lazy Load', () => {
|
|
13
|
+
const oldDoc = document
|
|
14
|
+
|
|
15
|
+
beforeAll(() => {
|
|
16
|
+
globalThis.document = new JSDOM().window.document
|
|
17
|
+
window.matchMedia = () => ({ matches: true } as any)
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
afterAll(() => {
|
|
21
|
+
globalThis.document = oldDoc
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
beforeEach(() => (document.body.innerHTML = '<div id="root"></div>'))
|
|
25
|
+
afterEach(() => (document.body.innerHTML = ''))
|
|
26
|
+
|
|
27
|
+
it('Shuld display the loader and completed state', async () => {
|
|
28
|
+
const injector = new Injector()
|
|
29
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
30
|
+
|
|
31
|
+
initializeShadeRoot({
|
|
32
|
+
injector,
|
|
33
|
+
rootElement,
|
|
34
|
+
jsxElement: (
|
|
35
|
+
<LazyLoad
|
|
36
|
+
loader={<div>Loading...</div>}
|
|
37
|
+
component={async () => {
|
|
38
|
+
await sleepAsync(100)
|
|
39
|
+
return <div>Loaded</div>
|
|
40
|
+
}}
|
|
41
|
+
/>
|
|
42
|
+
),
|
|
43
|
+
})
|
|
44
|
+
expect(document.body.innerHTML).toBe('<div id="root"><lazy-load><div>Loading...</div></lazy-load></div>')
|
|
45
|
+
await sleepAsync(150)
|
|
46
|
+
expect(document.body.innerHTML).toBe('<div id="root"><lazy-load><div>Loaded</div></lazy-load></div>')
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('Shuld display the failed state with a retryer', async () => {
|
|
50
|
+
const injector = new Injector()
|
|
51
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
52
|
+
|
|
53
|
+
const load = jest.fn(async () => {
|
|
54
|
+
throw Error(':(')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
initializeShadeRoot({
|
|
58
|
+
injector,
|
|
59
|
+
rootElement,
|
|
60
|
+
jsxElement: (
|
|
61
|
+
<LazyLoad
|
|
62
|
+
loader={<div>Loading...</div>}
|
|
63
|
+
component={load}
|
|
64
|
+
error={(e, retry) => (
|
|
65
|
+
<button id="retry" onclick={retry}>
|
|
66
|
+
{(e as Error).message}
|
|
67
|
+
</button>
|
|
68
|
+
)}
|
|
69
|
+
/>
|
|
70
|
+
),
|
|
71
|
+
})
|
|
72
|
+
expect(document.body.innerHTML).toBe('<div id="root"><lazy-load><div>Loading...</div></lazy-load></div>')
|
|
73
|
+
await sleepAsync(1)
|
|
74
|
+
expect(load).toBeCalledTimes(1)
|
|
75
|
+
expect(document.body.innerHTML).toBe('<div id="root"><lazy-load><button id="retry">:(</button></lazy-load></div>')
|
|
76
|
+
document.getElementById('retry')?.click()
|
|
77
|
+
expect(load).toBeCalledTimes(2)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('Shuld display the failed state with a retryer', async () => {
|
|
81
|
+
const injector = new Injector()
|
|
82
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
83
|
+
let counter = 0
|
|
84
|
+
|
|
85
|
+
const load = jest.fn(async () => {
|
|
86
|
+
if (!counter) {
|
|
87
|
+
counter += 1
|
|
88
|
+
throw Error(':(')
|
|
89
|
+
}
|
|
90
|
+
return <div>success</div>
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
initializeShadeRoot({
|
|
94
|
+
injector,
|
|
95
|
+
rootElement,
|
|
96
|
+
jsxElement: (
|
|
97
|
+
<LazyLoad
|
|
98
|
+
loader={<div>Loading...</div>}
|
|
99
|
+
component={load}
|
|
100
|
+
error={(e, retry) => (
|
|
101
|
+
<button id="retry" onclick={retry}>
|
|
102
|
+
{(e as Error).message}
|
|
103
|
+
</button>
|
|
104
|
+
)}
|
|
105
|
+
/>
|
|
106
|
+
),
|
|
107
|
+
})
|
|
108
|
+
expect(document.body.innerHTML).toBe('<div id="root"><lazy-load><div>Loading...</div></lazy-load></div>')
|
|
109
|
+
await sleepAsync(1)
|
|
110
|
+
expect(load).toBeCalledTimes(1)
|
|
111
|
+
expect(document.body.innerHTML).toBe('<div id="root"><lazy-load><button id="retry">:(</button></lazy-load></div>')
|
|
112
|
+
document.getElementById('retry')?.click()
|
|
113
|
+
expect(load).toBeCalledTimes(2)
|
|
114
|
+
await sleepAsync(1)
|
|
115
|
+
expect(document.body.innerHTML).toBe('<div id="root"><lazy-load><div>success</div></lazy-load></div>')
|
|
116
|
+
})
|
|
117
|
+
})
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { TextEncoder, TextDecoder } from 'util'
|
|
2
|
+
|
|
3
|
+
global.TextEncoder = TextEncoder
|
|
4
|
+
global.TextDecoder = TextDecoder as any
|
|
5
|
+
|
|
6
|
+
import { Injector } from '@furystack/inject'
|
|
7
|
+
import { RouteLink } from './route-link'
|
|
8
|
+
import { JSDOM } from 'jsdom'
|
|
9
|
+
import { createComponent, initializeShadeRoot, LocationService } from '..'
|
|
10
|
+
|
|
11
|
+
describe('RouteLink', () => {
|
|
12
|
+
const oldDoc = document
|
|
13
|
+
|
|
14
|
+
beforeAll(() => {
|
|
15
|
+
globalThis.document = new JSDOM().window.document
|
|
16
|
+
window.matchMedia = () => ({ matches: true } as any)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
afterAll(() => {
|
|
20
|
+
globalThis.document = oldDoc
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
beforeEach(() => (document.body.innerHTML = '<div id="root"></div>'))
|
|
24
|
+
afterEach(() => (document.body.innerHTML = ''))
|
|
25
|
+
|
|
26
|
+
it('Shuld display the loader and completed state', async () => {
|
|
27
|
+
const injector = new Injector()
|
|
28
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
29
|
+
|
|
30
|
+
const onRouteChange = jest.fn()
|
|
31
|
+
|
|
32
|
+
injector.getInstance(LocationService).onLocationChanged.subscribe(onRouteChange)
|
|
33
|
+
|
|
34
|
+
initializeShadeRoot({
|
|
35
|
+
injector,
|
|
36
|
+
rootElement,
|
|
37
|
+
jsxElement: (
|
|
38
|
+
<RouteLink id="route" href="/subroute">
|
|
39
|
+
Link
|
|
40
|
+
</RouteLink>
|
|
41
|
+
),
|
|
42
|
+
})
|
|
43
|
+
expect(document.body.innerHTML).toBe(
|
|
44
|
+
'<div id="root"><route-link><a id="route" href="/subroute">Link</a></route-link></div>',
|
|
45
|
+
)
|
|
46
|
+
expect(onRouteChange).not.toBeCalled()
|
|
47
|
+
document.getElementById('route')?.click()
|
|
48
|
+
expect(onRouteChange).toBeCalledTimes(1)
|
|
49
|
+
})
|
|
50
|
+
})
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { TextEncoder, TextDecoder } from 'util'
|
|
2
|
+
|
|
3
|
+
global.TextEncoder = TextEncoder
|
|
4
|
+
global.TextDecoder = TextDecoder as any
|
|
5
|
+
|
|
6
|
+
import { Injector } from '@furystack/inject'
|
|
7
|
+
import { Router } from './router'
|
|
8
|
+
import { JSDOM } from 'jsdom'
|
|
9
|
+
import { createComponent, initializeShadeRoot, LocationService } from '..'
|
|
10
|
+
import { RouteLink } from '.'
|
|
11
|
+
import { sleepAsync } from '@furystack/utils'
|
|
12
|
+
|
|
13
|
+
describe('Router', () => {
|
|
14
|
+
const oldDoc = document
|
|
15
|
+
|
|
16
|
+
beforeAll(() => {
|
|
17
|
+
globalThis.document = new JSDOM().window.document
|
|
18
|
+
window.matchMedia = () => ({ matches: true } as any)
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
afterAll(() => {
|
|
22
|
+
globalThis.document = oldDoc
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
beforeEach(() => (document.body.innerHTML = '<div id="root"></div>'))
|
|
26
|
+
afterEach(() => (document.body.innerHTML = ''))
|
|
27
|
+
|
|
28
|
+
it('Shuld display the loader and completed state', async () => {
|
|
29
|
+
history.pushState(null, '', '/')
|
|
30
|
+
|
|
31
|
+
const onVisit = jest.fn()
|
|
32
|
+
const onLeave = jest.fn()
|
|
33
|
+
const onLastLeave = jest.fn()
|
|
34
|
+
|
|
35
|
+
const injector = new Injector()
|
|
36
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
37
|
+
|
|
38
|
+
const onRouteChange = jest.fn()
|
|
39
|
+
|
|
40
|
+
injector.getInstance(LocationService).onLocationChanged.subscribe(onRouteChange)
|
|
41
|
+
|
|
42
|
+
initializeShadeRoot({
|
|
43
|
+
injector,
|
|
44
|
+
rootElement,
|
|
45
|
+
jsxElement: (
|
|
46
|
+
<div>
|
|
47
|
+
<RouteLink id="home" href="/">
|
|
48
|
+
home
|
|
49
|
+
</RouteLink>
|
|
50
|
+
<RouteLink id="a" href="/route-a">
|
|
51
|
+
a
|
|
52
|
+
</RouteLink>
|
|
53
|
+
<RouteLink id="b" href="/route-b">
|
|
54
|
+
b
|
|
55
|
+
</RouteLink>
|
|
56
|
+
<RouteLink id="b-with-id" href="/route-b/123">
|
|
57
|
+
b-with-id
|
|
58
|
+
</RouteLink>
|
|
59
|
+
<RouteLink id="c" href="/route-c">
|
|
60
|
+
c
|
|
61
|
+
</RouteLink>
|
|
62
|
+
<RouteLink id="x" href="/route-x">
|
|
63
|
+
x
|
|
64
|
+
</RouteLink>
|
|
65
|
+
<Router
|
|
66
|
+
routes={[
|
|
67
|
+
{ url: '/route-a', component: () => <div id="content">route-a</div>, onVisit, onLeave },
|
|
68
|
+
{ url: '/route-b/:id?', component: ({ match }) => <div id="content">route-b{match.params.id}</div> },
|
|
69
|
+
{
|
|
70
|
+
url: '/route-c',
|
|
71
|
+
component: () => <div id="content">route-c</div>,
|
|
72
|
+
onLeave: onLastLeave,
|
|
73
|
+
},
|
|
74
|
+
{ url: '/', component: () => <div id="content">home</div> },
|
|
75
|
+
]}
|
|
76
|
+
notFound={() => <div id="content">not found</div>}
|
|
77
|
+
/>
|
|
78
|
+
</div>
|
|
79
|
+
),
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
const getContent = () => document.getElementById('content')?.innerHTML
|
|
83
|
+
const getLocation = () => location.pathname
|
|
84
|
+
|
|
85
|
+
const clickOn = (name: string) => document.getElementById(name)?.click()
|
|
86
|
+
|
|
87
|
+
await sleepAsync(100)
|
|
88
|
+
|
|
89
|
+
expect(getLocation()).toBe('/')
|
|
90
|
+
expect(getContent()).toBe('home')
|
|
91
|
+
|
|
92
|
+
expect(onVisit).not.toBeCalled()
|
|
93
|
+
|
|
94
|
+
clickOn('a')
|
|
95
|
+
await sleepAsync(100)
|
|
96
|
+
expect(getContent()).toBe('route-a')
|
|
97
|
+
expect(getLocation()).toBe('/route-a')
|
|
98
|
+
expect(onRouteChange).toBeCalledTimes(1)
|
|
99
|
+
expect(onVisit).toBeCalledTimes(1)
|
|
100
|
+
|
|
101
|
+
clickOn('a')
|
|
102
|
+
await sleepAsync(100)
|
|
103
|
+
expect(onVisit).toBeCalledTimes(1)
|
|
104
|
+
expect(onLeave).not.toBeCalled()
|
|
105
|
+
|
|
106
|
+
clickOn('b')
|
|
107
|
+
await sleepAsync(100)
|
|
108
|
+
expect(onLeave).toBeCalledTimes(1)
|
|
109
|
+
|
|
110
|
+
expect(getContent()).toBe('route-b')
|
|
111
|
+
expect(getLocation()).toBe('/route-b')
|
|
112
|
+
|
|
113
|
+
clickOn('b-with-id')
|
|
114
|
+
await sleepAsync(100)
|
|
115
|
+
expect(getContent()).toBe('route-b123')
|
|
116
|
+
expect(getLocation()).toBe('/route-b/123')
|
|
117
|
+
|
|
118
|
+
clickOn('c')
|
|
119
|
+
await sleepAsync(100)
|
|
120
|
+
expect(getContent()).toBe('route-c')
|
|
121
|
+
expect(getLocation()).toBe('/route-c')
|
|
122
|
+
|
|
123
|
+
expect(onLastLeave).not.toBeCalled()
|
|
124
|
+
clickOn('x')
|
|
125
|
+
await sleepAsync(100)
|
|
126
|
+
expect(getContent()).toBe('not found')
|
|
127
|
+
expect(getLocation()).toBe('/route-x')
|
|
128
|
+
expect(onLastLeave).toBeCalledTimes(1)
|
|
129
|
+
})
|
|
130
|
+
})
|
|
@@ -3,10 +3,11 @@ import { createComponent } from '../shade-component'
|
|
|
3
3
|
import { LocationService } from '../services'
|
|
4
4
|
import { match, MatchResult, TokensToRegexpOptions } from 'path-to-regexp'
|
|
5
5
|
import { RenderOptions } from '../models'
|
|
6
|
+
import Semaphore from 'semaphore-async-await'
|
|
6
7
|
|
|
7
8
|
export interface Route<TMatchResult extends object> {
|
|
8
9
|
url: string
|
|
9
|
-
component: (options: { currentUrl:
|
|
10
|
+
component: (options: { currentUrl: string; match: MatchResult<TMatchResult> }) => JSX.Element
|
|
10
11
|
routingOptions?: TokensToRegexpOptions
|
|
11
12
|
onVisit?: (options: RenderOptions<RouterProps, RouterState>) => Promise<void>
|
|
12
13
|
onLeave?: (options: RenderOptions<RouterProps, RouterState>) => Promise<void>
|
|
@@ -15,47 +16,55 @@ export interface Route<TMatchResult extends object> {
|
|
|
15
16
|
export interface RouterProps {
|
|
16
17
|
style?: CSSStyleDeclaration
|
|
17
18
|
routes: Array<Route<any>>
|
|
18
|
-
notFound?: (currentUrl:
|
|
19
|
+
notFound?: (currentUrl: string) => JSX.Element
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
export interface RouterState {
|
|
22
23
|
activeRoute?: Route<any>
|
|
23
24
|
activeRouteParams?: any
|
|
24
25
|
jsx?: JSX.Element
|
|
26
|
+
lock: Semaphore
|
|
25
27
|
}
|
|
26
28
|
export const Router = Shade<RouterProps, RouterState>({
|
|
27
29
|
shadowDomName: 'shade-router',
|
|
28
|
-
getInitialState: () => ({
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
30
|
+
getInitialState: () => ({
|
|
31
|
+
lock: new Semaphore(1),
|
|
32
|
+
}),
|
|
33
|
+
resources: ({ children, props, injector, updateState, getState, element }) => [
|
|
34
|
+
injector.getInstance(LocationService).onLocationChanged.subscribe(async (currentUrl) => {
|
|
35
|
+
const { activeRoute: lastRoute, activeRouteParams: lastParams, lock } = getState()
|
|
36
|
+
try {
|
|
37
|
+
await lock.acquire()
|
|
38
|
+
|
|
39
|
+
for (const route of props.routes) {
|
|
40
|
+
const matchFn = match(route.url, route.routingOptions)
|
|
41
|
+
const matchResult = matchFn(currentUrl)
|
|
42
|
+
if (matchResult) {
|
|
43
|
+
if (route !== lastRoute || JSON.stringify(lastParams) !== JSON.stringify(matchResult.params)) {
|
|
44
|
+
if (lastRoute?.onLeave) {
|
|
45
|
+
await lastRoute.onLeave({ children, props, injector, updateState, getState, element })
|
|
46
|
+
}
|
|
47
|
+
updateState({
|
|
48
|
+
jsx: route.component({ currentUrl, match: matchResult }),
|
|
49
|
+
activeRoute: route,
|
|
50
|
+
activeRouteParams: matchResult.params,
|
|
51
|
+
})
|
|
52
|
+
if (route.onVisit) {
|
|
53
|
+
await route.onVisit({ children, props, injector, updateState, getState, element })
|
|
54
|
+
}
|
|
47
55
|
}
|
|
56
|
+
return
|
|
48
57
|
}
|
|
49
|
-
return
|
|
50
58
|
}
|
|
59
|
+
if (lastRoute?.onLeave) {
|
|
60
|
+
await lastRoute.onLeave({ children, props, injector, updateState, getState, element })
|
|
61
|
+
}
|
|
62
|
+
updateState({ jsx: props.notFound?.(currentUrl), activeRoute: undefined })
|
|
63
|
+
} finally {
|
|
64
|
+
lock.release()
|
|
51
65
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
}
|
|
55
|
-
updateState({ jsx: props.notFound?.(currentUrl), activeRoute: undefined })
|
|
56
|
-
}, true)
|
|
57
|
-
return () => subscription.dispose()
|
|
58
|
-
},
|
|
66
|
+
}, true),
|
|
67
|
+
],
|
|
59
68
|
render: ({ getState }) => {
|
|
60
69
|
const { jsx } = getState()
|
|
61
70
|
if (jsx) {
|
|
@@ -1,16 +1,50 @@
|
|
|
1
|
+
import { TextEncoder, TextDecoder } from 'util'
|
|
2
|
+
|
|
3
|
+
global.TextEncoder = TextEncoder
|
|
4
|
+
global.TextDecoder = TextDecoder as any
|
|
5
|
+
|
|
1
6
|
import { Injector } from '@furystack/inject'
|
|
2
7
|
import { usingAsync } from '@furystack/utils'
|
|
3
8
|
import { LocationService } from './'
|
|
4
9
|
import { JSDOM } from 'jsdom'
|
|
5
10
|
|
|
6
11
|
describe('LocationService', () => {
|
|
12
|
+
const oldDoc = document
|
|
13
|
+
|
|
14
|
+
beforeAll(() => {
|
|
15
|
+
globalThis.document = new JSDOM().window.document
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
afterAll(() => {
|
|
19
|
+
globalThis.document = oldDoc
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
beforeEach(() => (document.body.innerHTML = '<div id="root"></div>'))
|
|
23
|
+
afterEach(() => (document.body.innerHTML = ''))
|
|
24
|
+
|
|
7
25
|
it('Shuld be constructed', async () => {
|
|
8
26
|
await usingAsync(new Injector(), async (i) => {
|
|
9
|
-
const dom = new JSDOM()
|
|
10
|
-
;(global as any).window = dom.window
|
|
11
|
-
i.setExplicitInstance(dom.window, Window)
|
|
12
27
|
const s = i.getInstance(LocationService)
|
|
13
28
|
expect(s).toBeInstanceOf(LocationService)
|
|
14
29
|
})
|
|
15
30
|
})
|
|
31
|
+
|
|
32
|
+
it('Shuld update state on events', async () => {
|
|
33
|
+
await usingAsync(new Injector(), async (i) => {
|
|
34
|
+
const onLocaionChanged = jest.fn()
|
|
35
|
+
const s = i.getInstance(LocationService)
|
|
36
|
+
s.onLocationChanged.subscribe(onLocaionChanged)
|
|
37
|
+
expect(onLocaionChanged).toBeCalledTimes(0)
|
|
38
|
+
history.pushState(null, '', '/loc1')
|
|
39
|
+
expect(onLocaionChanged).toBeCalledTimes(1)
|
|
40
|
+
history.replaceState(null, '', '/loc2')
|
|
41
|
+
expect(onLocaionChanged).toBeCalledTimes(2)
|
|
42
|
+
|
|
43
|
+
// TODO: Figure out testing hashchange and popstate subscriptions
|
|
44
|
+
// window.dispatchEvent(new HashChangeEvent('hashchange', { newURL: '/loc3' }))
|
|
45
|
+
// expect(onLocaionChanged).toBeCalledTimes(3)
|
|
46
|
+
// window.dispatchEvent(new PopStateEvent('popstate', {}))
|
|
47
|
+
// expect(onLocaionChanged).toBeCalledTimes(4)
|
|
48
|
+
})
|
|
49
|
+
})
|
|
16
50
|
})
|
|
@@ -7,13 +7,14 @@ export class LocationService implements Disposable {
|
|
|
7
7
|
window.removeEventListener('hashchange', this.updateState)
|
|
8
8
|
this.pushStateTracer.dispose()
|
|
9
9
|
this.replaceStateTracer.dispose()
|
|
10
|
+
this.onLocationChanged.dispose()
|
|
10
11
|
}
|
|
11
12
|
|
|
12
|
-
public onLocationChanged = new ObservableValue
|
|
13
|
+
public onLocationChanged = new ObservableValue(new URL(location.href).pathname)
|
|
13
14
|
|
|
14
15
|
public updateState() {
|
|
15
16
|
const newUrl = new URL(location.href)
|
|
16
|
-
this.onLocationChanged.setValue(newUrl)
|
|
17
|
+
this.onLocationChanged.setValue(newUrl.pathname)
|
|
17
18
|
}
|
|
18
19
|
|
|
19
20
|
private pushStateTracer: Disposable
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { TextEncoder, TextDecoder } from 'util'
|
|
2
|
+
|
|
3
|
+
global.TextEncoder = TextEncoder
|
|
4
|
+
global.TextDecoder = TextDecoder as any
|
|
5
|
+
|
|
6
|
+
import { Injector } from '@furystack/inject'
|
|
7
|
+
import { usingAsync } from '@furystack/utils'
|
|
8
|
+
import { ScreenService } from './screen-service'
|
|
9
|
+
import { JSDOM } from 'jsdom'
|
|
10
|
+
|
|
11
|
+
describe('ScreenService', () => {
|
|
12
|
+
const oldDoc = document
|
|
13
|
+
|
|
14
|
+
beforeAll(() => {
|
|
15
|
+
globalThis.document = new JSDOM().window.document
|
|
16
|
+
window.matchMedia = () => ({ matches: true } as any)
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
afterAll(() => {
|
|
20
|
+
globalThis.document = oldDoc
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
beforeEach(() => (document.body.innerHTML = '<div id="root"></div>'))
|
|
24
|
+
afterEach(() => (document.body.innerHTML = ''))
|
|
25
|
+
|
|
26
|
+
it('Shuld be constructed', async () => {
|
|
27
|
+
await usingAsync(new Injector(), async (i) => {
|
|
28
|
+
const s = i.getInstance(ScreenService)
|
|
29
|
+
expect(s).toBeInstanceOf(ScreenService)
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('Shuld update state on events', async () => {
|
|
34
|
+
await usingAsync(new Injector(), async (i) => {
|
|
35
|
+
i.getInstance(ScreenService)
|
|
36
|
+
/** TODO */
|
|
37
|
+
})
|
|
38
|
+
})
|
|
39
|
+
})
|
|
@@ -8,7 +8,7 @@ export type ScreenSize = typeof ScreenSizes[number]
|
|
|
8
8
|
export type Breakpoint = { name: ScreenSize; minSize: number; maxSize?: number }
|
|
9
9
|
|
|
10
10
|
@Injectable({ lifetime: 'singleton' })
|
|
11
|
-
export class
|
|
11
|
+
export class ScreenService implements Disposable {
|
|
12
12
|
private getOrientation = () => (window.matchMedia('(orientation:landscape').matches ? 'landscape' : 'portrait')
|
|
13
13
|
|
|
14
14
|
public readonly breakpoints: { [K in ScreenSize]: { minSize: number } } = {
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { Injector } from '@furystack/inject'
|
|
2
|
+
|
|
3
|
+
import { TextEncoder, TextDecoder } from 'util'
|
|
4
|
+
|
|
5
|
+
global.TextEncoder = TextEncoder
|
|
6
|
+
global.TextDecoder = TextDecoder as any
|
|
7
|
+
|
|
8
|
+
import { JSDOM } from 'jsdom'
|
|
9
|
+
import { initializeShadeRoot } from './initialize'
|
|
10
|
+
import { Shade } from './shade'
|
|
11
|
+
import { createComponent } from './shade-component'
|
|
12
|
+
import { ObservableValue } from '@furystack/utils'
|
|
13
|
+
|
|
14
|
+
describe('Shade Resources integration tests', () => {
|
|
15
|
+
const oldDoc = document
|
|
16
|
+
|
|
17
|
+
beforeAll(() => {
|
|
18
|
+
globalThis.document = new JSDOM().window.document
|
|
19
|
+
})
|
|
20
|
+
|
|
21
|
+
afterAll(() => {
|
|
22
|
+
globalThis.document = oldDoc
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
beforeEach(() => (document.body.innerHTML = '<div id="root"></div>'))
|
|
26
|
+
afterEach(() => (document.body.innerHTML = ''))
|
|
27
|
+
|
|
28
|
+
it('Should update the component based on a custom observable value change', () => {
|
|
29
|
+
const injector = new Injector()
|
|
30
|
+
const rootElement = document.getElementById('root') as HTMLDivElement
|
|
31
|
+
|
|
32
|
+
const renderCounter = jest.fn()
|
|
33
|
+
|
|
34
|
+
const obs1 = new ObservableValue(0)
|
|
35
|
+
const obs2 = new ObservableValue('a')
|
|
36
|
+
|
|
37
|
+
const ExampleComponent = Shade({
|
|
38
|
+
resources: ({ element }) => [
|
|
39
|
+
obs1.subscribe(
|
|
40
|
+
(val1) => ((element.querySelector('#val1') as HTMLDivElement).innerHTML = val1.toString()),
|
|
41
|
+
true,
|
|
42
|
+
),
|
|
43
|
+
obs2.subscribe(
|
|
44
|
+
(val2) => ((element.querySelector('#val2') as HTMLDivElement).innerHTML = val2.toString()),
|
|
45
|
+
true,
|
|
46
|
+
),
|
|
47
|
+
],
|
|
48
|
+
render: () => {
|
|
49
|
+
renderCounter()
|
|
50
|
+
return (
|
|
51
|
+
<div>
|
|
52
|
+
<div id="val1"></div>
|
|
53
|
+
<div id="val2"></div>
|
|
54
|
+
</div>
|
|
55
|
+
)
|
|
56
|
+
},
|
|
57
|
+
shadowDomName: 'shades-example-resource',
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
expect(obs1.getObservers().length).toBe(0)
|
|
61
|
+
expect(obs2.getObservers().length).toBe(0)
|
|
62
|
+
|
|
63
|
+
initializeShadeRoot({
|
|
64
|
+
injector,
|
|
65
|
+
rootElement,
|
|
66
|
+
jsxElement: <ExampleComponent />,
|
|
67
|
+
})
|
|
68
|
+
expect(document.body.innerHTML).toBe(
|
|
69
|
+
'<div id="root"><shades-example-resource><div><div id="val1">0</div><div id="val2">a</div></div></shades-example-resource></div>',
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
expect(obs1.getObservers().length).toBe(1)
|
|
73
|
+
expect(obs2.getObservers().length).toBe(1)
|
|
74
|
+
|
|
75
|
+
expect(renderCounter).toBeCalledTimes(1)
|
|
76
|
+
|
|
77
|
+
obs1.setValue(1)
|
|
78
|
+
expect(document.body.innerHTML).toBe(
|
|
79
|
+
'<div id="root"><shades-example-resource><div><div id="val1">1</div><div id="val2">a</div></div></shades-example-resource></div>',
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
obs2.setValue('b')
|
|
83
|
+
expect(document.body.innerHTML).toBe(
|
|
84
|
+
'<div id="root"><shades-example-resource><div><div id="val1">1</div><div id="val2">b</div></div></shades-example-resource></div>',
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
document.body.innerHTML = ''
|
|
88
|
+
|
|
89
|
+
expect(obs1.getObservers().length).toBe(0)
|
|
90
|
+
expect(obs2.getObservers().length).toBe(0)
|
|
91
|
+
|
|
92
|
+
expect(renderCounter).toBeCalledTimes(1)
|
|
93
|
+
})
|
|
94
|
+
})
|