UNPKG

2.61 kBPlain TextView Raw
1import {memoize} from '@shopify/decorators';
2
3enum SupportedDimension {
4 OffsetWidth = 'offsetWidth',
5 OffsetHeight = 'offsetHeight',
6 ScrollWidth = 'scrollWidth',
7 ScrollHeight = 'scrollHeight',
8}
9
10type MockedGetter = (element: HTMLElement) => number;
11type Mock = MockedGetter | number;
12type Mocks = Partial<Record<string, Mock>>;
13
14type AugmentedElement = Element & {[key: string]: Mock};
15
16interface NativeImplentationMap {
17 [key: string]: Element;
18}
19
20function isGetterFunction(mock?: Mock): mock is MockedGetter {
21 return mock != null && typeof mock === 'function';
22}
23
24export default class Dimension {
25 private isUsingMock = false;
26 private overwrittenImplementations: string[] = [];
27
28 mock(mocks: Mocks) {
29 if (this.isUsingMock) {
30 throw new Error(
31 'Dimensions are already mocked, but you tried to mock them again.',
32 );
33 } else if (Object.keys(mocks).length === 0) {
34 throw new Error('No dimensions provided for mocking');
35 }
36
37 this.mockDOMMethods(mocks);
38 this.isUsingMock = true;
39 }
40
41 restore() {
42 if (!this.isUsingMock) {
43 throw new Error(
44 "Dimensions haven't been mocked, but you are trying to restore them.",
45 );
46 }
47
48 this.restoreDOMMethods();
49 this.isUsingMock = false;
50 }
51
52 isMocked() {
53 return this.isUsingMock;
54 }
55
56 @memoize()
57 private get nativeImplementations(): NativeImplentationMap {
58 return {
59 [SupportedDimension.OffsetWidth]: HTMLElement.prototype,
60 [SupportedDimension.OffsetHeight]: HTMLElement.prototype,
61 [SupportedDimension.ScrollWidth]: Element.prototype,
62 [SupportedDimension.ScrollHeight]: Element.prototype,
63 };
64 }
65
66 private mockDOMMethods(mocks: Mocks) {
67 Object.keys(mocks).forEach(method => {
68 const nativeSource = this.nativeImplementations[method];
69 const mock: Mock | undefined = mocks[method];
70
71 this.overwrittenImplementations.push(method);
72
73 if (isGetterFunction(mock)) {
74 Object.defineProperty(nativeSource, method, {
75 get() {
76 return mock.call(this, this);
77 },
78 configurable: true,
79 });
80 } else {
81 Object.defineProperty(nativeSource, method, {
82 value: mocks[method],
83 configurable: true,
84 });
85 }
86 });
87 }
88
89 private restoreDOMMethods() {
90 this.overwrittenImplementations.forEach(method => {
91 const nativeSource = this.nativeImplementations[method];
92
93 if (nativeSource == null) {
94 return;
95 }
96
97 delete (nativeSource as AugmentedElement)[method];
98 });
99
100 this.overwrittenImplementations = [];
101 }
102}