@servicetitan/web-components 22.15.0 → 22.16.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/dist/common.d.ts.map +1 -1
- package/dist/common.js +12 -7
- package/dist/common.js.map +1 -1
- package/dist/loader.d.ts.map +1 -1
- package/dist/loader.js +2 -0
- package/dist/loader.js.map +1 -1
- package/dist/mfe-data-context.d.ts.map +1 -1
- package/dist/mfe-data-context.js +0 -3
- package/dist/mfe-data-context.js.map +1 -1
- package/package.json +8 -7
- package/src/__tests__/common.test.ts +257 -65
- package/src/__tests__/event-bus.test.ts +146 -0
- package/src/__tests__/globals.test.ts +20 -0
- package/src/__tests__/history-manager.test.ts +111 -0
- package/src/__tests__/loader.test.tsx +638 -0
- package/src/__tests__/mfe-data-context.test.tsx +28 -0
- package/src/__tests__/register.test.tsx +135 -12
- package/src/__tests__/utils.test.ts +84 -90
- package/src/common.ts +9 -2
- package/src/loader.tsx +2 -0
- package/src/mfe-data-context.ts +0 -4
|
@@ -1,22 +1,25 @@
|
|
|
1
|
-
import { FC } from 'react';
|
|
1
|
+
import { FC, useContext } from 'react';
|
|
2
|
+
import { TablePopupPropsContext } from '@servicetitan/design-system';
|
|
2
3
|
import { render } from '@testing-library/react';
|
|
3
4
|
import { screen } from 'shadow-dom-testing-library';
|
|
4
5
|
import '@testing-library/jest-dom';
|
|
5
6
|
|
|
6
|
-
import { register } from '../register';
|
|
7
7
|
import { IWebComponent } from '../common';
|
|
8
8
|
import { useMFEDataContext } from '../mfe-data-context';
|
|
9
9
|
|
|
10
|
-
describe(
|
|
11
|
-
|
|
10
|
+
describe('[web-components] register', () => {
|
|
11
|
+
let onloadRefs: Function[];
|
|
12
|
+
const componentName = 'mock-app';
|
|
12
13
|
const handleRef = (webComponent: IWebComponent | null) => {
|
|
13
14
|
webComponent?.provide({
|
|
14
|
-
onReady: ()
|
|
15
|
-
onDispose: ()
|
|
15
|
+
onReady: jest.fn(),
|
|
16
|
+
onDispose: jest.fn(),
|
|
16
17
|
});
|
|
17
18
|
};
|
|
18
19
|
const TestComponent: FC<Record<string, any>> = props => {
|
|
19
20
|
const data = useMFEDataContext();
|
|
21
|
+
const popupProps = useContext(TablePopupPropsContext);
|
|
22
|
+
|
|
20
23
|
return (
|
|
21
24
|
<div>
|
|
22
25
|
{Object.entries(props).map(([name, value]) => (
|
|
@@ -25,30 +28,125 @@ describe(`[web-component] ${register.name}`, () => {
|
|
|
25
28
|
{Object.entries(data).map(([name, value]) => (
|
|
26
29
|
<span key={name}>{`Data: ${name}, Value: ${JSON.stringify(value)}`}</span>
|
|
27
30
|
))}
|
|
31
|
+
<span>TablePopupPropsContext appendTo: {String(popupProps({}).appendTo)}</span>
|
|
28
32
|
</div>
|
|
29
33
|
);
|
|
30
34
|
};
|
|
31
35
|
|
|
36
|
+
class Page {
|
|
37
|
+
static get componentShadowRoot() {
|
|
38
|
+
return document.querySelector(componentName)!.shadowRoot!;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static get portalShadowRoot() {
|
|
42
|
+
return document.querySelector('body')!.lastElementChild!.shadowRoot!;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
32
46
|
beforeAll(() => {
|
|
33
47
|
/*
|
|
34
48
|
* This register can only happen once, unfortunately. So the TestComponent needs to
|
|
35
49
|
* work for all tests. If ran more than once, window.customElements.define will throw
|
|
36
50
|
* the error: "NotSupportedError: This name has already been registered in the registry."
|
|
37
51
|
*
|
|
38
|
-
* The other issue is that the global WEB_COMPONENT_NAME can only be set in the jest config
|
|
39
|
-
* via package.json, so we can only have one WEB_COMPONENT_NAME name for the test suite.
|
|
40
|
-
*
|
|
41
52
|
* Also note that there is not way to "unregister" a web component, so an afterAll
|
|
42
53
|
* cannot be used.
|
|
43
54
|
*/
|
|
55
|
+
(global as any).WEB_COMPONENT_NAME = componentName;
|
|
56
|
+
const { register } = require('../register');
|
|
44
57
|
register(TestComponent, true);
|
|
58
|
+
|
|
59
|
+
/*
|
|
60
|
+
* mock <link>'s onload so that we can call it directly from the test,
|
|
61
|
+
* because it won't call itself in the test environment
|
|
62
|
+
*/
|
|
63
|
+
Object.defineProperty(HTMLLinkElement.prototype, 'onload', {
|
|
64
|
+
get() {
|
|
65
|
+
return this._onload;
|
|
66
|
+
},
|
|
67
|
+
set(onload: Function) {
|
|
68
|
+
onloadRefs.push(onload);
|
|
69
|
+
this._onload = onload;
|
|
70
|
+
},
|
|
71
|
+
});
|
|
45
72
|
});
|
|
46
73
|
|
|
47
|
-
|
|
74
|
+
beforeEach(() => {
|
|
75
|
+
onloadRefs = [];
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const subject = (props: Record<string, any> = {}, dataAttrs: Record<string, any> = {}) => {
|
|
48
79
|
const data = props && { 'data-mfe-data': JSON.stringify(props) };
|
|
49
|
-
|
|
80
|
+
const Component: any = componentName;
|
|
81
|
+
const renderResult = render(<Component ref={handleRef} {...data} {...dataAttrs} />);
|
|
82
|
+
|
|
83
|
+
for (const onload of onloadRefs) {
|
|
84
|
+
onload();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return renderResult;
|
|
50
88
|
};
|
|
51
89
|
|
|
90
|
+
describe('when component has style urls', () => {
|
|
91
|
+
let styleUrls: string[];
|
|
92
|
+
|
|
93
|
+
beforeEach(() => {
|
|
94
|
+
styleUrls = ['foo.css', 'bar.css'];
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
function expectStylesInDom(styleUrls: string[], shadowRootFn: () => ShadowRoot) {
|
|
98
|
+
expect(shadowRootFn().querySelectorAll('link').length).toBe(styleUrls.length);
|
|
99
|
+
for (const styleUrl of styleUrls) {
|
|
100
|
+
const link = shadowRootFn().querySelector(
|
|
101
|
+
`link[href='${styleUrl}'][rel='stylesheet'][crossorigin='anonymous']`
|
|
102
|
+
);
|
|
103
|
+
expect(link).toBeInTheDocument();
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
test('loads the styles into the component shadow DOM', () => {
|
|
108
|
+
subject({}, { 'style-urls': JSON.stringify(styleUrls) });
|
|
109
|
+
expectStylesInDom(styleUrls, () => Page.componentShadowRoot);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('loads the styles into the portal shadow DOM', () => {
|
|
113
|
+
subject({}, { 'style-urls': JSON.stringify(styleUrls) });
|
|
114
|
+
expectStylesInDom(styleUrls, () => Page.portalShadowRoot);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
describe('when a design system link is present in the DOM', () => {
|
|
118
|
+
let designSystemLink: HTMLLinkElement;
|
|
119
|
+
|
|
120
|
+
beforeEach(() => {
|
|
121
|
+
designSystemLink = Object.assign(document.createElement('link'), {
|
|
122
|
+
href: 'http://localhost/design-system.foobar.bundle.css',
|
|
123
|
+
rel: 'stylesheet',
|
|
124
|
+
});
|
|
125
|
+
document.body.appendChild(designSystemLink);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
afterEach(() => {
|
|
129
|
+
designSystemLink.remove();
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
test('loads the styles and design system styles into the component shadow DOM', () => {
|
|
133
|
+
subject({}, { 'style-urls': JSON.stringify(styleUrls) });
|
|
134
|
+
expectStylesInDom(
|
|
135
|
+
[...styleUrls, designSystemLink.href],
|
|
136
|
+
() => Page.componentShadowRoot
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
test('loads the styles and design system styles into the portal shadow DOM', () => {
|
|
141
|
+
subject({}, { 'style-urls': JSON.stringify(styleUrls) });
|
|
142
|
+
expectStylesInDom(
|
|
143
|
+
[...styleUrls, designSystemLink.href],
|
|
144
|
+
() => Page.portalShadowRoot
|
|
145
|
+
);
|
|
146
|
+
});
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
52
150
|
describe('when component has custom data-attributes', () => {
|
|
53
151
|
const props = {
|
|
54
152
|
string: 'foo',
|
|
@@ -81,7 +179,7 @@ describe(`[web-component] ${register.name}`, () => {
|
|
|
81
179
|
const setup = () => {
|
|
82
180
|
subject(props);
|
|
83
181
|
|
|
84
|
-
const webComponent = document.getElementsByTagName(
|
|
182
|
+
const webComponent = document.getElementsByTagName(componentName);
|
|
85
183
|
webComponent[0]?.setAttribute(
|
|
86
184
|
'data-mfe-data',
|
|
87
185
|
JSON.stringify({ ...props, string: 'baz' })
|
|
@@ -96,4 +194,29 @@ describe(`[web-component] ${register.name}`, () => {
|
|
|
96
194
|
});
|
|
97
195
|
});
|
|
98
196
|
});
|
|
197
|
+
|
|
198
|
+
describe('when WEB_COMPONENT_NAME is not set', () => {
|
|
199
|
+
let register: typeof import('../register').register;
|
|
200
|
+
|
|
201
|
+
beforeEach(() => {
|
|
202
|
+
jest.isolateModules(() => {
|
|
203
|
+
(global as any).WEB_COMPONENT_NAME = undefined;
|
|
204
|
+
register = require('../register').register;
|
|
205
|
+
});
|
|
206
|
+
jest.spyOn(console, 'error').mockImplementation(jest.fn());
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
test('logs an error', () => {
|
|
210
|
+
register(TestComponent, true);
|
|
211
|
+
// eslint-disable-next-line no-console
|
|
212
|
+
expect(console.error).toHaveBeenCalledWith('"WEB_COMPONENT_NAME" is not defined!');
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
test('configures the shadow root in PopupPropsContext', () => {
|
|
217
|
+
subject();
|
|
218
|
+
expect(screen.getByShadowText(/TablePopupPropsContext appendTo:/)).toHaveTextContent(
|
|
219
|
+
/[object ShadowRoot]/
|
|
220
|
+
);
|
|
221
|
+
});
|
|
99
222
|
});
|
|
@@ -1,101 +1,95 @@
|
|
|
1
|
+
import { ExposedDependencies } from '../common';
|
|
1
2
|
import { getVersionMismatches, isVersionedUrl } from '../utils';
|
|
2
3
|
|
|
3
|
-
describe('
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
'mobx': 'SharedDependencies.MobX',
|
|
9
|
-
'mobx-react': 'SharedDependencies.MobXReact',
|
|
10
|
-
'mobx-utils': 'SharedDependencies.MobXUtils',
|
|
11
|
-
'react': 'SharedDependencies.React',
|
|
12
|
-
'react-dom': 'SharedDependencies.ReactDOM',
|
|
13
|
-
});
|
|
4
|
+
describe('[web-components] utils', () => {
|
|
5
|
+
describe(`${getVersionMismatches.name}`, () => {
|
|
6
|
+
let sharedDependencies: Record<string, string> = {};
|
|
7
|
+
let dependencies: Record<string, string> = {};
|
|
8
|
+
let exposedDependencies: ExposedDependencies = {};
|
|
14
9
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
'@servicetitan/design-system': '~11.8.0',
|
|
20
|
-
'@servicetitan/form': '^21.7.1',
|
|
21
|
-
'@servicetitan/log-service': '^21.7.1',
|
|
22
|
-
'@servicetitan/react-ioc': '^21.7.1',
|
|
23
|
-
'@servicetitan/tokens': '~12.1.11',
|
|
24
|
-
'@servicetitan/web-components': '^21.7.1',
|
|
25
|
-
'accounting': '~0.4.1',
|
|
26
|
-
'axios': '~0.25.0',
|
|
27
|
-
'classnames': '~2.3.1',
|
|
28
|
-
'formstate': '^2.0.0',
|
|
29
|
-
'mobx': '~6.6.0',
|
|
30
|
-
'mobx-react': '~7.5.0',
|
|
31
|
-
'react': '~17.0.2',
|
|
32
|
-
'react-dom': '~17.0.2',
|
|
33
|
-
'react-router-dom': '~6.3.0',
|
|
34
|
-
'resumablejs': '1.0.2',
|
|
35
|
-
'utils': '^0.15.9',
|
|
36
|
-
});
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
sharedDependencies = {
|
|
12
|
+
'@servicetitan/design-system': 'SharedDependencies.ServiceTitan.DesignSystem',
|
|
13
|
+
};
|
|
37
14
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
variable: 'SharedDependencies.FormState',
|
|
50
|
-
},
|
|
51
|
-
'mobx': {
|
|
52
|
-
version: '~6.6.0',
|
|
53
|
-
variable: 'SharedDependencies.MobX',
|
|
54
|
-
},
|
|
55
|
-
'mobx-react': {
|
|
56
|
-
version: '~7.5.0',
|
|
57
|
-
variable: 'SharedDependencies.MobXReact',
|
|
58
|
-
},
|
|
59
|
-
'mobx-utils': {
|
|
60
|
-
version: '~6.0.4',
|
|
61
|
-
variable: 'SharedDependencies.MobXUtils',
|
|
62
|
-
},
|
|
63
|
-
'react': {
|
|
64
|
-
version: '~17.0.2',
|
|
65
|
-
variable: 'SharedDependencies.React',
|
|
66
|
-
},
|
|
67
|
-
'react-dom': {
|
|
68
|
-
version: '~17.0.2',
|
|
69
|
-
variable: 'SharedDependencies.ReactDOM',
|
|
70
|
-
},
|
|
71
|
-
});
|
|
15
|
+
dependencies = {
|
|
16
|
+
'@servicetitan/design-system': '~11.8.0',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
exposedDependencies = {
|
|
20
|
+
'@servicetitan/design-system': {
|
|
21
|
+
version: '~11.8.0',
|
|
22
|
+
variable: 'SharedDependencies.ServiceTitan.DesignSystem',
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
});
|
|
72
26
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
27
|
+
test('returns no mismatches', () => {
|
|
28
|
+
expect(
|
|
29
|
+
getVersionMismatches(exposedDependencies, dependencies, sharedDependencies)
|
|
30
|
+
).toEqual({});
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('when there is a versions mismatch', () => {
|
|
34
|
+
beforeEach(() => (dependencies['@servicetitan/design-system'] = '~12.0.0'));
|
|
35
|
+
|
|
36
|
+
test('returns one mismatch', () => {
|
|
37
|
+
expect(
|
|
38
|
+
getVersionMismatches(exposedDependencies, dependencies, sharedDependencies)
|
|
39
|
+
).toEqual({
|
|
40
|
+
'@servicetitan/design-system': {
|
|
41
|
+
host: '~11.8.0',
|
|
42
|
+
package: '~12.0.0',
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('when a dependency is missing from exposed dependencies', () => {
|
|
49
|
+
beforeEach(() => delete exposedDependencies['@servicetitan/design-system']);
|
|
50
|
+
|
|
51
|
+
test('returns an object citing the dependency is missing from the host', () => {
|
|
52
|
+
expect(
|
|
53
|
+
getVersionMismatches(exposedDependencies, dependencies, sharedDependencies)
|
|
54
|
+
).toEqual({
|
|
55
|
+
'@servicetitan/design-system': {
|
|
56
|
+
host: 'missing',
|
|
57
|
+
package: '~11.8.0',
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe('when a variable does not match', () => {
|
|
64
|
+
beforeEach(() => (exposedDependencies['@servicetitan/design-system'].variable = 'foo'));
|
|
65
|
+
|
|
66
|
+
test('returns an object citing the variable is wrong', () => {
|
|
67
|
+
expect(
|
|
68
|
+
getVersionMismatches(exposedDependencies, dependencies, sharedDependencies)
|
|
69
|
+
).toEqual({
|
|
70
|
+
'@servicetitan/design-system': {
|
|
71
|
+
host: 'wrong global variable',
|
|
72
|
+
package: '~11.8.0',
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
});
|
|
81
77
|
});
|
|
82
|
-
});
|
|
83
78
|
|
|
84
|
-
describe(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
79
|
+
describe(`${isVersionedUrl.name}`, () => {
|
|
80
|
+
const results: [string, boolean][] = [
|
|
81
|
+
['localhost:9090', false],
|
|
82
|
+
['localhost:9090@1.1.1', false],
|
|
83
|
+
['localhost:9090@dev', false],
|
|
84
|
+
['//unpkg.servicetitan.com/@servicetitan/test', false],
|
|
85
|
+
['//unpkg.servicetitan.com/@servicetitan/test@dev', false],
|
|
86
|
+
['//unpkg.servicetitan.com/@servicetitan/test@0.0.0', true],
|
|
87
|
+
['//unpkg.servicetitan.com/@servicetitan/test@0.0.0-hash', true],
|
|
88
|
+
['//unpkg.servicetitan.com/@servicetitan/test@^16', false],
|
|
89
|
+
];
|
|
95
90
|
|
|
96
|
-
|
|
97
|
-
test(`versioned url check for '${url}' should be ${result}`, () => {
|
|
91
|
+
test.each(results)(`providing %s returns %s`, (url, result) => {
|
|
98
92
|
expect(isVersionedUrl(url)).toEqual(result);
|
|
99
93
|
});
|
|
100
|
-
}
|
|
94
|
+
});
|
|
101
95
|
});
|
package/src/common.ts
CHANGED
|
@@ -93,19 +93,26 @@ export async function getJson<T = any>(
|
|
|
93
93
|
try {
|
|
94
94
|
response = await fetch(url, config);
|
|
95
95
|
} catch {
|
|
96
|
-
if (retries > 0) {
|
|
96
|
+
if (retries > 0 && !config.signal?.aborted) {
|
|
97
97
|
return new Promise((resolve, reject) => {
|
|
98
98
|
const { signal } = config;
|
|
99
99
|
|
|
100
100
|
const timeoutId = window.setTimeout(() => {
|
|
101
101
|
getJson(url, {
|
|
102
102
|
...config,
|
|
103
|
+
onRequestError,
|
|
103
104
|
retries: retries - 1,
|
|
104
105
|
retryDelay: retryDelay * 4,
|
|
105
106
|
}).then(resolve, reject);
|
|
106
107
|
}, retryDelay);
|
|
107
108
|
|
|
108
|
-
|
|
109
|
+
const abortHandler = () => {
|
|
110
|
+
window.clearTimeout(timeoutId);
|
|
111
|
+
signal?.removeEventListener('abort', abortHandler);
|
|
112
|
+
reject(signal?.reason);
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
signal?.addEventListener('abort', abortHandler);
|
|
109
116
|
});
|
|
110
117
|
}
|
|
111
118
|
|
package/src/loader.tsx
CHANGED
|
@@ -264,6 +264,7 @@ export const Loader: <T>(props: LoaderProps<T> & { ref?: Ref<LoaderRef> }) => Re
|
|
|
264
264
|
script.crossOrigin = 'anonymous';
|
|
265
265
|
script.onload = () => {
|
|
266
266
|
script.dataset.webComponent = WebComponent;
|
|
267
|
+
script.onload = null;
|
|
267
268
|
resolve();
|
|
268
269
|
};
|
|
269
270
|
script.async = true;
|
|
@@ -276,6 +277,7 @@ export const Loader: <T>(props: LoaderProps<T> & { ref?: Ref<LoaderRef> }) => Re
|
|
|
276
277
|
const originalOnload = script.onload;
|
|
277
278
|
script.onload = (...args) => {
|
|
278
279
|
originalOnload?.call(script, ...args);
|
|
280
|
+
script.onload = null;
|
|
279
281
|
resolve();
|
|
280
282
|
};
|
|
281
283
|
});
|
package/src/mfe-data-context.ts
CHANGED
|
@@ -25,9 +25,5 @@ export const MFEDataContext = createContext<JSON>({});
|
|
|
25
25
|
export function useMFEDataContext<T extends MFEContextData<T>>() {
|
|
26
26
|
const context = useContext(MFEDataContext) as T;
|
|
27
27
|
|
|
28
|
-
if (!context) {
|
|
29
|
-
throw new Error('useMFEDataContext must be used within a MFEDataContext.Provider');
|
|
30
|
-
}
|
|
31
|
-
|
|
32
28
|
return context;
|
|
33
29
|
}
|