@servicetitan/web-components 22.14.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 +9 -8
- 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 -13
- 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
|
@@ -0,0 +1,638 @@
|
|
|
1
|
+
import { Loader, LoaderProps } from '../loader';
|
|
2
|
+
import { act, render, screen, waitFor } from '@testing-library/react';
|
|
3
|
+
import '@testing-library/jest-dom';
|
|
4
|
+
import { Entries, EXPOSED_DEPENDENCIES_TOKEN } from '../common';
|
|
5
|
+
import * as common from '../common';
|
|
6
|
+
import * as utils from '../utils';
|
|
7
|
+
import { FC } from 'react';
|
|
8
|
+
import { Provider, ProviderProps } from '@servicetitan/react-ioc';
|
|
9
|
+
import { Log } from '@servicetitan/log-service';
|
|
10
|
+
import { HistoryManager } from '../history-manager';
|
|
11
|
+
import { EVENT_BUS_TOKEN } from '../event-bus';
|
|
12
|
+
|
|
13
|
+
describe('[web-components] Loader', () => {
|
|
14
|
+
const baseUrl = 'http://localhost/mfe';
|
|
15
|
+
const WEB_COMPONENT_NAME = 'mfe-component';
|
|
16
|
+
let webComponentState: Partial<Entries>;
|
|
17
|
+
let webComponent: WebComponent | undefined;
|
|
18
|
+
class WebComponent extends HTMLElement {
|
|
19
|
+
provide = (entries: Entries) => {
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
21
|
+
webComponent = this;
|
|
22
|
+
for (const [key, value] of Object.entries(entries)) {
|
|
23
|
+
/*
|
|
24
|
+
* set values to external state so that we can call or
|
|
25
|
+
* check any of the values in the test
|
|
26
|
+
*/
|
|
27
|
+
webComponentState[key as keyof Entries] = value;
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let props: LoaderProps<any>;
|
|
33
|
+
let onloadRef: Function | null;
|
|
34
|
+
let singletons: ProviderProps['singletons'];
|
|
35
|
+
let getJsonMockFns: Record<string, Function>;
|
|
36
|
+
|
|
37
|
+
let metadata: {
|
|
38
|
+
name: string;
|
|
39
|
+
bundledWith?: Record<string, string | undefined>;
|
|
40
|
+
sharedDependencies?: Record<string, string>;
|
|
41
|
+
dependencies?: Record<string, string>;
|
|
42
|
+
entrypoints?: Record<string, { css: string[]; js: string[] }>;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
function setStartupVersion(version: string | undefined) {
|
|
46
|
+
if (!version) {
|
|
47
|
+
metadata.bundledWith = undefined;
|
|
48
|
+
} else {
|
|
49
|
+
metadata.bundledWith = { '@servicetitan/startup': version };
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
beforeAll(() => {
|
|
54
|
+
// register web component in order to test the `provide` method
|
|
55
|
+
window.customElements.define(WEB_COMPONENT_NAME, WebComponent);
|
|
56
|
+
|
|
57
|
+
/*
|
|
58
|
+
* mock <script>'s onload so that we can call it directly from the test,
|
|
59
|
+
* because it won't call itself in the test environment
|
|
60
|
+
*/
|
|
61
|
+
Object.defineProperty(HTMLScriptElement.prototype, 'onload', {
|
|
62
|
+
get() {
|
|
63
|
+
return this._onload;
|
|
64
|
+
},
|
|
65
|
+
set(onload: Function) {
|
|
66
|
+
onloadRef = onload;
|
|
67
|
+
this._onload = onload;
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
function initializeGetJsonMockFns(url = baseUrl) {
|
|
73
|
+
getJsonMockFns = {
|
|
74
|
+
[`${url}/dist/metadata.json`]: () => {
|
|
75
|
+
return Promise.resolve({
|
|
76
|
+
data: metadata,
|
|
77
|
+
headers: {} as Headers,
|
|
78
|
+
url,
|
|
79
|
+
});
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
beforeEach(() => {
|
|
85
|
+
jest.resetAllMocks();
|
|
86
|
+
document.querySelectorAll('script').forEach(script => script.remove());
|
|
87
|
+
|
|
88
|
+
webComponentState = {};
|
|
89
|
+
webComponent = undefined;
|
|
90
|
+
metadata = {
|
|
91
|
+
name: WEB_COMPONENT_NAME,
|
|
92
|
+
sharedDependencies: {},
|
|
93
|
+
dependencies: {},
|
|
94
|
+
entrypoints: {
|
|
95
|
+
full: {
|
|
96
|
+
css: ['full.css'],
|
|
97
|
+
js: ['full.js'],
|
|
98
|
+
},
|
|
99
|
+
light: {
|
|
100
|
+
css: ['light.css'],
|
|
101
|
+
js: ['light.js'],
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
setStartupVersion('22.14.0');
|
|
106
|
+
props = { src: baseUrl };
|
|
107
|
+
onloadRef = null;
|
|
108
|
+
singletons = [];
|
|
109
|
+
initializeGetJsonMockFns();
|
|
110
|
+
|
|
111
|
+
jest.spyOn(common, 'getJson').mockImplementation((url, _) => {
|
|
112
|
+
if (getJsonMockFns[url] === undefined) {
|
|
113
|
+
throw new Error(`Unexpected getJson call with url: ${url}`);
|
|
114
|
+
}
|
|
115
|
+
return getJsonMockFns[url]();
|
|
116
|
+
});
|
|
117
|
+
jest.spyOn(utils, 'getVersionMismatches').mockReturnValue({});
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
function subject() {
|
|
121
|
+
return render(
|
|
122
|
+
<Provider singletons={singletons}>
|
|
123
|
+
<Loader {...props} />
|
|
124
|
+
</Provider>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
test('renders default loading fallback', () => {
|
|
129
|
+
subject();
|
|
130
|
+
expect(screen.getByText(/loading/i)).toBeInTheDocument();
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
describe('when loadingFallback is provided', () => {
|
|
134
|
+
beforeEach(() => {
|
|
135
|
+
props.loadingFallback = <div>foo</div>;
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
test('renders provided loading fallback', () => {
|
|
139
|
+
subject();
|
|
140
|
+
expect(screen.getByText('foo')).toBeInTheDocument();
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe('when the loadingFallback throws an error', () => {
|
|
145
|
+
beforeEach(() => {
|
|
146
|
+
// suppress console.error that appears in the test output
|
|
147
|
+
jest.spyOn(console, 'error').mockImplementation(jest.fn);
|
|
148
|
+
const ThrowError: FC = () => {
|
|
149
|
+
throw new Error('woops');
|
|
150
|
+
};
|
|
151
|
+
props.loadingFallback = (
|
|
152
|
+
<div>
|
|
153
|
+
<ThrowError />
|
|
154
|
+
</div>
|
|
155
|
+
);
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
test('renders default error fallback', () => {
|
|
159
|
+
subject();
|
|
160
|
+
expect(screen.getByText(/something went wrong/i)).toBeInTheDocument();
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
describe('when an errorFallback is provided', () => {
|
|
164
|
+
beforeEach(() => {
|
|
165
|
+
props.errorFallback = <div>bar</div>;
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test('renders provided error fallback', () => {
|
|
169
|
+
subject();
|
|
170
|
+
expect(screen.getByText('bar')).toBeInTheDocument();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe('when a script tag (with a webComponent data attr) for this bundle is in the DOM before Loader is rendered', () => {
|
|
176
|
+
beforeEach(() => {
|
|
177
|
+
const script = document.createElement('script');
|
|
178
|
+
script.setAttribute('src', `${baseUrl}/dist/bundle/light/light.js`);
|
|
179
|
+
script.dataset.webComponent = 'foo';
|
|
180
|
+
document.body.appendChild(script);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
/*
|
|
184
|
+
* when a script tag is already in the dom, we don't need to manually trigger the onload,
|
|
185
|
+
* as we do with the remaining tests
|
|
186
|
+
*/
|
|
187
|
+
test('renders web component', async () => {
|
|
188
|
+
subject();
|
|
189
|
+
await waitFor(() =>
|
|
190
|
+
expect(document.querySelector(WEB_COMPONENT_NAME)).toBeInTheDocument()
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
describe('after the script tag added is loaded', () => {
|
|
196
|
+
async function scriptLoadedSetup() {
|
|
197
|
+
subject();
|
|
198
|
+
|
|
199
|
+
await waitFor(() => onloadRef !== undefined);
|
|
200
|
+
act(() => {
|
|
201
|
+
onloadRef!();
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
await waitFor(() =>
|
|
205
|
+
expect(document.querySelector(WEB_COMPONENT_NAME)).toBeInTheDocument()
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function itAddsJsEntrypointToWebComponent(entrypointTestText: string, urlFn: () => string) {
|
|
210
|
+
test(`adds the ${entrypointTestText} to a script tag`, async () => {
|
|
211
|
+
await scriptLoadedSetup();
|
|
212
|
+
const script = document.querySelector('script');
|
|
213
|
+
expect(script).toHaveAttribute('src', urlFn());
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function itAddsCssEntrypointToWebComponent(
|
|
218
|
+
entrypointTestText: string,
|
|
219
|
+
urlFn: () => string
|
|
220
|
+
) {
|
|
221
|
+
test(`adds the ${entrypointTestText} to the web component style-urls attribute`, async () => {
|
|
222
|
+
await scriptLoadedSetup();
|
|
223
|
+
expect(webComponent).toHaveAttribute(
|
|
224
|
+
'style-urls',
|
|
225
|
+
encodeURIComponent(JSON.stringify([urlFn()]))
|
|
226
|
+
);
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function itCallsGetJsonWithUrlAndCacheValue(
|
|
231
|
+
urlFn: () => string,
|
|
232
|
+
cache: string | undefined
|
|
233
|
+
) {
|
|
234
|
+
test(`getJson is called with url and cache set to ${cache}`, async () => {
|
|
235
|
+
await scriptLoadedSetup();
|
|
236
|
+
expect(common.getJson).toHaveBeenCalledWith(
|
|
237
|
+
urlFn(),
|
|
238
|
+
expect.objectContaining({ cache })
|
|
239
|
+
);
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
describe('when the metadata contains no dependencies prop', () => {
|
|
244
|
+
const dependencies = 'baz';
|
|
245
|
+
function addGetJsonPackageJsonMockFn(url: string) {
|
|
246
|
+
getJsonMockFns[`${url}/package.json`] = () => ({
|
|
247
|
+
data: { dependencies },
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
beforeEach(() => {
|
|
252
|
+
metadata.dependencies = undefined;
|
|
253
|
+
addGetJsonPackageJsonMockFn(baseUrl);
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
test('dependencies are pulled from package.json', async () => {
|
|
257
|
+
await scriptLoadedSetup();
|
|
258
|
+
expect(utils.getVersionMismatches).toHaveBeenCalledWith(
|
|
259
|
+
expect.anything(),
|
|
260
|
+
dependencies,
|
|
261
|
+
expect.anything()
|
|
262
|
+
);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
itCallsGetJsonWithUrlAndCacheValue(() => `${baseUrl}/package.json`, 'no-store');
|
|
266
|
+
|
|
267
|
+
describe('when baseUrl is versioned', () => {
|
|
268
|
+
const versionedBaseUrl = 'http://unpkg.localhost/mfe@v1.0.0';
|
|
269
|
+
|
|
270
|
+
beforeEach(() => {
|
|
271
|
+
props = { src: versionedBaseUrl };
|
|
272
|
+
initializeGetJsonMockFns(versionedBaseUrl);
|
|
273
|
+
addGetJsonPackageJsonMockFn(versionedBaseUrl);
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
itCallsGetJsonWithUrlAndCacheValue(
|
|
277
|
+
() => `${versionedBaseUrl}/package.json`,
|
|
278
|
+
undefined
|
|
279
|
+
);
|
|
280
|
+
});
|
|
281
|
+
});
|
|
282
|
+
|
|
283
|
+
describe('when the startup version is < 22.12.0', () => {
|
|
284
|
+
beforeEach(() => {
|
|
285
|
+
setStartupVersion('22.11.0');
|
|
286
|
+
props.data = { fooBar: 'foobar' };
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
test('data is spread as data attributes in kebab case on the web component', async () => {
|
|
290
|
+
await scriptLoadedSetup();
|
|
291
|
+
expect(webComponent).toHaveAttribute('data-foo-bar', 'foobar');
|
|
292
|
+
});
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
itAddsJsEntrypointToWebComponent(
|
|
296
|
+
'light js entrypoint from metadata.json',
|
|
297
|
+
() => `${baseUrl}/dist/bundle/light/${metadata.entrypoints!.light.js[0]}`
|
|
298
|
+
);
|
|
299
|
+
|
|
300
|
+
itAddsCssEntrypointToWebComponent(
|
|
301
|
+
'light css entrypoint from metadata.json',
|
|
302
|
+
() => `${baseUrl}/dist/bundle/light/${metadata.entrypoints!.light.css[0]}`
|
|
303
|
+
);
|
|
304
|
+
|
|
305
|
+
describe('when the startup version is < 17.0.0', () => {
|
|
306
|
+
beforeEach(() => {
|
|
307
|
+
setStartupVersion('16.0.0');
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
itAddsJsEntrypointToWebComponent(
|
|
311
|
+
'full js entrypoint from metadata.json',
|
|
312
|
+
() => `${baseUrl}/dist/bundle/full/${metadata.entrypoints!.full.js[0]}`
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
itAddsCssEntrypointToWebComponent(
|
|
316
|
+
'full css entrypoint from metadata.json',
|
|
317
|
+
() => `${baseUrl}/dist/bundle/full/${metadata.entrypoints!.full.css[0]}`
|
|
318
|
+
);
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
describe('when the startup version is missing', () => {
|
|
322
|
+
beforeEach(() => {
|
|
323
|
+
setStartupVersion(undefined);
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
itAddsJsEntrypointToWebComponent(
|
|
327
|
+
'full js entrypoint from metadata.json',
|
|
328
|
+
() => `${baseUrl}/dist/bundle/full/${metadata.entrypoints!.full.js[0]}`
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
itAddsCssEntrypointToWebComponent(
|
|
332
|
+
'full css entrypoint from metadata.json',
|
|
333
|
+
() => `${baseUrl}/dist/bundle/full/${metadata.entrypoints!.full.css[0]}`
|
|
334
|
+
);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
itCallsGetJsonWithUrlAndCacheValue(() => `${baseUrl}/dist/metadata.json`, 'no-store');
|
|
338
|
+
|
|
339
|
+
describe('when baseUrl is versioned', () => {
|
|
340
|
+
const versionedBaseUrl = 'http://unpkg.localhost/mfe@v1.0.0';
|
|
341
|
+
|
|
342
|
+
beforeEach(() => {
|
|
343
|
+
props.src = versionedBaseUrl;
|
|
344
|
+
initializeGetJsonMockFns(versionedBaseUrl);
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
itCallsGetJsonWithUrlAndCacheValue(
|
|
348
|
+
() => `${versionedBaseUrl}/dist/metadata.json`,
|
|
349
|
+
undefined
|
|
350
|
+
);
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
describe('when the metadata request fails', () => {
|
|
354
|
+
const fallbackCssEntrypoint = 'foo.css';
|
|
355
|
+
const fallbackJsEntrypoint = 'bar.js';
|
|
356
|
+
|
|
357
|
+
beforeEach(() => {
|
|
358
|
+
getJsonMockFns[`${baseUrl}/dist/metadata.json`] = () => {
|
|
359
|
+
return Promise.reject();
|
|
360
|
+
};
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test('throws an error', async () => {
|
|
364
|
+
await expect(scriptLoadedSetup()).rejects.toThrow();
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
describe('when the fallback url is provided', () => {
|
|
368
|
+
function addGetJsonFallbackMockFn(url: string) {
|
|
369
|
+
getJsonMockFns[`${url}/dist/metadata.json`] = () => {
|
|
370
|
+
const fallbackMetadata = { ...metadata };
|
|
371
|
+
fallbackMetadata.entrypoints = {
|
|
372
|
+
light: {
|
|
373
|
+
css: [fallbackCssEntrypoint],
|
|
374
|
+
js: [fallbackJsEntrypoint],
|
|
375
|
+
},
|
|
376
|
+
};
|
|
377
|
+
return Promise.resolve({
|
|
378
|
+
data: fallbackMetadata,
|
|
379
|
+
headers: {} as Headers,
|
|
380
|
+
url,
|
|
381
|
+
});
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
const fallbackSrc = 'http://localhost/mfe-fallback';
|
|
385
|
+
|
|
386
|
+
beforeEach(() => {
|
|
387
|
+
props.fallbackSrc = fallbackSrc;
|
|
388
|
+
addGetJsonFallbackMockFn(fallbackSrc);
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
itAddsJsEntrypointToWebComponent(
|
|
392
|
+
"js entrypoint from the fallback url's metadata.json",
|
|
393
|
+
() => `${fallbackSrc}/dist/bundle/light/${fallbackJsEntrypoint}`
|
|
394
|
+
);
|
|
395
|
+
|
|
396
|
+
itAddsCssEntrypointToWebComponent(
|
|
397
|
+
"css entrypoint from the fallback url's metadata.json",
|
|
398
|
+
() => `${fallbackSrc}/dist/bundle/light/${fallbackCssEntrypoint}`
|
|
399
|
+
);
|
|
400
|
+
|
|
401
|
+
itCallsGetJsonWithUrlAndCacheValue(
|
|
402
|
+
() => `${fallbackSrc}/dist/metadata.json`,
|
|
403
|
+
'no-store'
|
|
404
|
+
);
|
|
405
|
+
|
|
406
|
+
describe('when fallback url is versioned', () => {
|
|
407
|
+
const fallbackSrc = 'http://unpkg.localhost/mfe-fallback@v1.0.0';
|
|
408
|
+
|
|
409
|
+
beforeEach(() => {
|
|
410
|
+
props.fallbackSrc = fallbackSrc;
|
|
411
|
+
addGetJsonFallbackMockFn(fallbackSrc);
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
itCallsGetJsonWithUrlAndCacheValue(
|
|
415
|
+
() => `${fallbackSrc}/dist/metadata.json`,
|
|
416
|
+
undefined
|
|
417
|
+
);
|
|
418
|
+
});
|
|
419
|
+
});
|
|
420
|
+
});
|
|
421
|
+
|
|
422
|
+
describe('when the metadata does not contain entrypoints', () => {
|
|
423
|
+
const cssEntrypoint = 'foo.css';
|
|
424
|
+
const jsEntrypoint = 'bar.js';
|
|
425
|
+
beforeEach(() => {
|
|
426
|
+
metadata.entrypoints = undefined;
|
|
427
|
+
getJsonMockFns[`${baseUrl}/dist/bundle/light/entrypoints.json`] = () => {
|
|
428
|
+
return Promise.resolve({
|
|
429
|
+
data: { css: [cssEntrypoint], js: [jsEntrypoint] },
|
|
430
|
+
});
|
|
431
|
+
};
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
itAddsJsEntrypointToWebComponent(
|
|
435
|
+
'js entrypoint from entrypoints.json',
|
|
436
|
+
() => `${baseUrl}/dist/bundle/light/${jsEntrypoint}`
|
|
437
|
+
);
|
|
438
|
+
|
|
439
|
+
itAddsCssEntrypointToWebComponent(
|
|
440
|
+
'css entrypoint from entrypoints.json',
|
|
441
|
+
() => `${baseUrl}/dist/bundle/light/${cssEntrypoint}`
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
describe('when entrypoints.json is missing', () => {
|
|
445
|
+
beforeEach(() => {
|
|
446
|
+
getJsonMockFns[`${baseUrl}/dist/bundle/light/entrypoints.json`] = () => {
|
|
447
|
+
return Promise.reject();
|
|
448
|
+
};
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
itAddsJsEntrypointToWebComponent(
|
|
452
|
+
'default js entrypoint',
|
|
453
|
+
() => `${baseUrl}/dist/bundle/light/index.js`
|
|
454
|
+
);
|
|
455
|
+
|
|
456
|
+
itAddsCssEntrypointToWebComponent(
|
|
457
|
+
'default css entrypoint',
|
|
458
|
+
() => `${baseUrl}/dist/bundle/light/index.css`
|
|
459
|
+
);
|
|
460
|
+
});
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
test('renders web component', async () => {
|
|
464
|
+
await scriptLoadedSetup();
|
|
465
|
+
expect(document.querySelector(WEB_COMPONENT_NAME)).toBeInTheDocument();
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
describe('after the web component is ready', () => {
|
|
469
|
+
async function webComponentReadySetup() {
|
|
470
|
+
await scriptLoadedSetup();
|
|
471
|
+
|
|
472
|
+
act(() => {
|
|
473
|
+
webComponentState.onReady!();
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
test('removes loading fallback', async () => {
|
|
478
|
+
await webComponentReadySetup();
|
|
479
|
+
expect(screen.queryByText(/loading/i)).not.toBeInTheDocument();
|
|
480
|
+
});
|
|
481
|
+
|
|
482
|
+
describe('when data is provided', () => {
|
|
483
|
+
beforeEach(() => {
|
|
484
|
+
props.data = { foo: 'bar' };
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
test('provides stringified data to the web component', async () => {
|
|
488
|
+
await webComponentReadySetup();
|
|
489
|
+
expect(webComponent).toHaveAttribute(
|
|
490
|
+
'data-mfe-data',
|
|
491
|
+
JSON.stringify(props.data)
|
|
492
|
+
);
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
|
|
496
|
+
describe('when a className is provided', () => {
|
|
497
|
+
beforeEach(() => {
|
|
498
|
+
props.className = 'foobar';
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
test('provides the className to the web component', async () => {
|
|
502
|
+
await webComponentReadySetup();
|
|
503
|
+
expect(webComponent).toHaveClass('foobar');
|
|
504
|
+
});
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
(
|
|
508
|
+
[
|
|
509
|
+
[Log, 'logService'],
|
|
510
|
+
[HistoryManager, 'historyManager'],
|
|
511
|
+
[EXPOSED_DEPENDENCIES_TOKEN, 'exposedDependencies'],
|
|
512
|
+
[EVENT_BUS_TOKEN, 'eventBus'],
|
|
513
|
+
] as [any, string][]
|
|
514
|
+
).forEach(([klass, propName]) => {
|
|
515
|
+
describe(`when ${propName} is provided to the parent <Provider>`, () => {
|
|
516
|
+
beforeEach(() => {
|
|
517
|
+
singletons?.push({ provide: klass, useValue: propName });
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
test(`provides ${propName} to the web component`, async () => {
|
|
521
|
+
await webComponentReadySetup();
|
|
522
|
+
expect(webComponentState[propName as keyof Entries]).toEqual(propName);
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
describe('when onDispose is called from the web component', () => {
|
|
528
|
+
async function onDisposeSetup() {
|
|
529
|
+
await webComponentReadySetup();
|
|
530
|
+
webComponentState.onDispose!();
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const clearSpy = jest.fn();
|
|
534
|
+
|
|
535
|
+
beforeEach(() => {
|
|
536
|
+
Object.defineProperty(window, 'SharedDependencies.ServiceTitan.DesignSystem', {
|
|
537
|
+
value: {
|
|
538
|
+
getStorage: () => ({
|
|
539
|
+
clear: clearSpy,
|
|
540
|
+
}),
|
|
541
|
+
},
|
|
542
|
+
configurable: true,
|
|
543
|
+
});
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
test('storage is not cleared', async () => {
|
|
547
|
+
await onDisposeSetup();
|
|
548
|
+
expect(clearSpy).not.toHaveBeenCalledWith(WEB_COMPONENT_NAME);
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
describe('when peerDependencies are provided', () => {
|
|
552
|
+
beforeEach(() => {
|
|
553
|
+
metadata.sharedDependencies = {
|
|
554
|
+
'@servicetitan/design-system':
|
|
555
|
+
'SharedDependencies.ServiceTitan.DesignSystem',
|
|
556
|
+
};
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
test('storage is cleared', async () => {
|
|
560
|
+
await onDisposeSetup();
|
|
561
|
+
expect(clearSpy).toHaveBeenCalledWith(WEB_COMPONENT_NAME);
|
|
562
|
+
});
|
|
563
|
+
});
|
|
564
|
+
});
|
|
565
|
+
|
|
566
|
+
describe('when a script tag (without a webComponent data attr) for this bundle is in the DOM before Loader is rendered', () => {
|
|
567
|
+
const originalOnloadSpy = jest.fn();
|
|
568
|
+
|
|
569
|
+
beforeEach(() => {
|
|
570
|
+
const script = document.createElement('script');
|
|
571
|
+
script.setAttribute('src', `${baseUrl}/dist/bundle/light/light.js`);
|
|
572
|
+
script.onload = originalOnloadSpy;
|
|
573
|
+
document.body.appendChild(script);
|
|
574
|
+
});
|
|
575
|
+
|
|
576
|
+
test("the script tag's original onload is called", async () => {
|
|
577
|
+
await webComponentReadySetup();
|
|
578
|
+
expect(originalOnloadSpy).toHaveBeenCalled();
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
test('onload handler gets unassigned after it gets called', async () => {
|
|
582
|
+
await webComponentReadySetup();
|
|
583
|
+
expect(onloadRef).toBeNull();
|
|
584
|
+
});
|
|
585
|
+
});
|
|
586
|
+
|
|
587
|
+
describe('when there are version mismatches', () => {
|
|
588
|
+
const mismatchedDeps = {
|
|
589
|
+
foo: {
|
|
590
|
+
host: 'bar',
|
|
591
|
+
package: 'baz',
|
|
592
|
+
},
|
|
593
|
+
};
|
|
594
|
+
|
|
595
|
+
beforeEach(() => {
|
|
596
|
+
jest.spyOn(utils, 'getVersionMismatches').mockReturnValue(mismatchedDeps);
|
|
597
|
+
});
|
|
598
|
+
|
|
599
|
+
itAddsJsEntrypointToWebComponent(
|
|
600
|
+
'full js entrypoint from metadata.json',
|
|
601
|
+
() => `${baseUrl}/dist/bundle/full/${metadata.entrypoints!.full.js[0]}`
|
|
602
|
+
);
|
|
603
|
+
|
|
604
|
+
itAddsCssEntrypointToWebComponent(
|
|
605
|
+
'full css entrypoint from metadata.json',
|
|
606
|
+
() => `${baseUrl}/dist/bundle/full/${metadata.entrypoints!.full.css[0]}`
|
|
607
|
+
);
|
|
608
|
+
|
|
609
|
+
describe('when logService is provided', () => {
|
|
610
|
+
const logService = { warning: jest.fn() };
|
|
611
|
+
|
|
612
|
+
beforeEach(() => {
|
|
613
|
+
singletons?.push({ provide: Log, useValue: logService });
|
|
614
|
+
});
|
|
615
|
+
|
|
616
|
+
test('logs a warning', async () => {
|
|
617
|
+
await webComponentReadySetup();
|
|
618
|
+
|
|
619
|
+
expect(logService.warning).toHaveBeenCalledWith(
|
|
620
|
+
expect.objectContaining({
|
|
621
|
+
category: 'MicroFrontends.DependenciesMismatch',
|
|
622
|
+
data: {
|
|
623
|
+
package: baseUrl,
|
|
624
|
+
dependencies: JSON.stringify(mismatchedDeps),
|
|
625
|
+
},
|
|
626
|
+
})
|
|
627
|
+
);
|
|
628
|
+
});
|
|
629
|
+
});
|
|
630
|
+
});
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
test('onload handler gets unassigned after it gets called', async () => {
|
|
634
|
+
await scriptLoadedSetup();
|
|
635
|
+
expect(onloadRef).toBeNull();
|
|
636
|
+
});
|
|
637
|
+
});
|
|
638
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { FC } from 'react';
|
|
2
|
+
import { renderHook } from '@testing-library/react-hooks';
|
|
3
|
+
import { MFEDataContext, useMFEDataContext } from '../mfe-data-context';
|
|
4
|
+
|
|
5
|
+
describe(`[web-components] ${useMFEDataContext.name}`, () => {
|
|
6
|
+
const subject = (wrapper?: FC) => renderHook(() => useMFEDataContext(), { wrapper });
|
|
7
|
+
|
|
8
|
+
describe('when the MFEDataContext is provided by a parent', () => {
|
|
9
|
+
const data = { foo: 'bar' };
|
|
10
|
+
const Wrapper: FC = ({ children }) => {
|
|
11
|
+
return <MFEDataContext.Provider value={data}>{children}</MFEDataContext.Provider>;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
test('returns the context', () => {
|
|
15
|
+
expect(subject(Wrapper).result.current).toEqual(data);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe('when the MFEDataContext is not provided by a parent', () => {
|
|
20
|
+
const Wrapper: FC = ({ children }) => {
|
|
21
|
+
return <div>{children}</div>;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
test('returns an empty object', () => {
|
|
25
|
+
expect(subject(Wrapper).result.current).toEqual({});
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
});
|