@furystack/shades 3.7.3 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/dist/component-factory.spec.js +68 -11
  2. package/dist/component-factory.spec.js.map +1 -1
  3. package/dist/components/lazy-load.spec.d.ts +2 -0
  4. package/dist/components/lazy-load.spec.d.ts.map +1 -0
  5. package/dist/components/lazy-load.spec.js +83 -0
  6. package/dist/components/lazy-load.spec.js.map +1 -0
  7. package/dist/components/route-link.d.ts.map +1 -1
  8. package/dist/components/route-link.js.map +1 -1
  9. package/dist/components/route-link.spec.d.ts +2 -0
  10. package/dist/components/route-link.spec.d.ts.map +1 -0
  11. package/dist/components/route-link.spec.js +38 -0
  12. package/dist/components/route-link.spec.js.map +1 -0
  13. package/dist/components/router.d.ts +4 -2
  14. package/dist/components/router.d.ts.map +1 -1
  15. package/dist/components/router.js +35 -26
  16. package/dist/components/router.js.map +1 -1
  17. package/dist/components/router.spec.d.ts +2 -0
  18. package/dist/components/router.spec.d.ts.map +1 -0
  19. package/dist/components/router.spec.js +93 -0
  20. package/dist/components/router.spec.js.map +1 -0
  21. package/dist/services/location-service.d.ts +1 -1
  22. package/dist/services/location-service.d.ts.map +1 -1
  23. package/dist/services/location-service.js +3 -2
  24. package/dist/services/location-service.js.map +1 -1
  25. package/dist/services/location-service.spec.js +29 -3
  26. package/dist/services/location-service.spec.js.map +1 -1
  27. package/dist/services/screen-service.d.ts +1 -1
  28. package/dist/services/screen-service.d.ts.map +1 -1
  29. package/dist/services/screen-service.js +5 -5
  30. package/dist/services/screen-service.js.map +1 -1
  31. package/dist/services/screen-service.spec.d.ts +2 -0
  32. package/dist/services/screen-service.spec.d.ts.map +1 -0
  33. package/dist/services/screen-service.spec.js +34 -0
  34. package/dist/services/screen-service.spec.js.map +1 -0
  35. package/dist/shade-resources.integration.spec.d.ts +2 -0
  36. package/dist/shade-resources.integration.spec.d.ts.map +1 -0
  37. package/dist/shade-resources.integration.spec.js +62 -0
  38. package/dist/shade-resources.integration.spec.js.map +1 -0
  39. package/dist/shade.d.ts +2 -0
  40. package/dist/shade.d.ts.map +1 -1
  41. package/dist/shade.js +22 -6
  42. package/dist/shade.js.map +1 -1
  43. package/dist/shades.integration.spec.d.ts +2 -0
  44. package/dist/shades.integration.spec.d.ts.map +1 -0
  45. package/dist/shades.integration.spec.js +123 -0
  46. package/dist/shades.integration.spec.js.map +1 -0
  47. package/package.json +8 -5
  48. package/src/component-factory.spec.tsx +97 -14
  49. package/src/components/lazy-load.spec.tsx +117 -0
  50. package/src/components/route-link.spec.tsx +50 -0
  51. package/src/components/route-link.tsx +2 -1
  52. package/src/components/router.spec.tsx +130 -0
  53. package/src/components/router.tsx +38 -29
  54. package/src/services/location-service.spec.ts +37 -3
  55. package/src/services/location-service.tsx +3 -2
  56. package/src/services/screen-service.spec.ts +39 -0
  57. package/src/services/screen-service.ts +1 -1
  58. package/src/shade-resources.integration.spec.tsx +94 -0
  59. package/src/shade.ts +29 -9
  60. 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
+ })
@@ -13,7 +13,8 @@ export const RouteLink = Shade<PartialElement<HTMLAnchorElement>>({
13
13
  ev.preventDefault()
14
14
  history.pushState('', props.title || '', props.href)
15
15
  injector.getInstance(LocationService).updateState()
16
- }}>
16
+ }}
17
+ >
17
18
  {children}
18
19
  </a>
19
20
  )
@@ -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: URL; match: MatchResult<TMatchResult> }) => JSX.Element
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: URL) => JSX.Element
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
- constructed: ({ children, props, injector, updateState, getState, element }) => {
30
- const subscription = injector.getInstance(LocationService).onLocationChanged.subscribe(async (currentUrl) => {
31
- const { activeRoute: lastRoute, activeRouteParams: lastParams } = getState()
32
- for (const route of props.routes) {
33
- const matchFn = match(route.url, route.routingOptions)
34
- const matchResult = matchFn(currentUrl.pathname)
35
- if (matchResult) {
36
- if (route !== lastRoute || JSON.stringify(lastParams) !== JSON.stringify(matchResult.params)) {
37
- if (lastRoute?.onLeave) {
38
- await lastRoute.onLeave({ children, props, injector, updateState, getState, element })
39
- }
40
- updateState({
41
- jsx: route.component({ currentUrl, match: matchResult }),
42
- activeRoute: route,
43
- activeRouteParams: matchResult.params,
44
- })
45
- if (route.onVisit) {
46
- await route.onVisit({ children, props, injector, updateState, getState, element })
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
- if (lastRoute?.onLeave) {
53
- await lastRoute.onLeave({ children, props, injector, updateState, getState, element })
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<URL>(new URL(location.href))
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 Screen implements Disposable {
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
+ })