1 | import {memoize} from '@shopify/decorators';
|
2 |
|
3 | enum SupportedDimension {
|
4 | OffsetWidth = 'offsetWidth',
|
5 | OffsetHeight = 'offsetHeight',
|
6 | ScrollWidth = 'scrollWidth',
|
7 | ScrollHeight = 'scrollHeight',
|
8 | }
|
9 |
|
10 | type MockedGetter = (element: HTMLElement) => number;
|
11 | type Mock = MockedGetter | number;
|
12 | type Mocks = Partial<Record<string, Mock>>;
|
13 |
|
14 | type AugmentedElement = Element & {[key: string]: Mock};
|
15 |
|
16 | interface NativeImplentationMap {
|
17 | [key: string]: Element;
|
18 | }
|
19 |
|
20 | function isGetterFunction(mock?: Mock): mock is MockedGetter {
|
21 | return mock != null && typeof mock === 'function';
|
22 | }
|
23 |
|
24 | export 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 | }
|