@hubspot/ui-extensions-dev-server 0.10.2 → 1.0.1
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/README.md +23 -4
- package/dist/index.d.ts +3 -3
- package/dist/index.js +4 -45
- package/dist/lib/DevModeInterface.d.ts +2 -2
- package/dist/lib/DevModeInterface.js +12 -28
- package/dist/lib/DevModeParentInterface.d.ts +2 -2
- package/dist/lib/DevModeParentInterface.js +138 -154
- package/dist/lib/DevModeUnifiedInterface.d.ts +2 -2
- package/dist/lib/DevModeUnifiedInterface.js +28 -49
- package/dist/lib/DevServerState.d.ts +9 -5
- package/dist/lib/DevServerState.js +37 -18
- package/dist/lib/ExtensionsWebSocket.d.ts +25 -0
- package/dist/lib/ExtensionsWebSocket.js +110 -0
- package/dist/lib/__mocks__/config.d.ts +2 -0
- package/dist/lib/__mocks__/config.js +5 -0
- package/dist/lib/__mocks__/isExtensionFile.d.ts +5 -0
- package/dist/lib/__mocks__/isExtensionFile.js +11 -0
- package/dist/lib/__tests__/DevModeInterface.spec.d.ts +1 -0
- package/dist/lib/__tests__/DevModeInterface.spec.js +155 -0
- package/dist/lib/__tests__/DevModeParentInterface.spec.d.ts +1 -0
- package/dist/lib/__tests__/DevModeParentInterface.spec.js +179 -0
- package/dist/lib/__tests__/DevModeUnifiedInterface.spec.d.ts +1 -0
- package/dist/lib/__tests__/DevModeUnifiedInterface.spec.js +236 -0
- package/dist/lib/__tests__/ExtensionsWebSocket.spec.d.ts +1 -0
- package/dist/lib/__tests__/ExtensionsWebSocket.spec.js +304 -0
- package/dist/lib/__tests__/ast.spec.d.ts +1 -0
- package/dist/lib/__tests__/ast.spec.js +737 -0
- package/dist/lib/__tests__/build.spec.d.ts +1 -0
- package/dist/lib/__tests__/build.spec.js +159 -0
- package/dist/lib/__tests__/config.spec.d.ts +1 -0
- package/dist/lib/__tests__/config.spec.js +291 -0
- package/dist/lib/__tests__/dev.spec.d.ts +1 -0
- package/dist/lib/__tests__/dev.spec.js +80 -0
- package/dist/lib/__tests__/extensionsService.spec.d.ts +1 -0
- package/dist/lib/__tests__/extensionsService.spec.js +150 -0
- package/dist/lib/__tests__/factories.d.ts +48 -0
- package/dist/lib/__tests__/factories.js +32 -0
- package/dist/lib/__tests__/fixtures/extensionConfig.d.ts +182 -0
- package/dist/lib/__tests__/fixtures/extensionConfig.js +304 -0
- package/dist/lib/__tests__/fixtures/urls.d.ts +4 -0
- package/dist/lib/__tests__/fixtures/urls.js +4 -0
- package/dist/lib/__tests__/parsing-utils.spec.d.ts +1 -0
- package/dist/lib/__tests__/parsing-utils.spec.js +467 -0
- package/dist/lib/__tests__/plugins/codeBlockingPlugin.spec.d.ts +1 -0
- package/dist/lib/__tests__/plugins/codeBlockingPlugin.spec.js +112 -0
- package/dist/lib/__tests__/plugins/codeCheckingPlugin.spec.d.ts +1 -0
- package/dist/lib/__tests__/plugins/codeCheckingPlugin.spec.js +82 -0
- package/dist/lib/__tests__/plugins/devBuildPlugin.spec.d.ts +1 -0
- package/dist/lib/__tests__/plugins/devBuildPlugin.spec.js +256 -0
- package/dist/lib/__tests__/plugins/friendlyLoggingPlugin.spec.d.ts +1 -0
- package/dist/lib/__tests__/plugins/friendlyLoggingPlugin.spec.js +65 -0
- package/dist/lib/__tests__/plugins/manifestPlugin.spec.d.ts +1 -0
- package/dist/lib/__tests__/plugins/manifestPlugin.spec.js +455 -0
- package/dist/lib/__tests__/plugins/relevantModulesPlugin.spec.d.ts +1 -0
- package/dist/lib/__tests__/plugins/relevantModulesPlugin.spec.js +81 -0
- package/dist/lib/__tests__/server.spec.d.ts +1 -0
- package/dist/lib/__tests__/server.spec.js +152 -0
- package/dist/lib/__tests__/test-utils/ast.d.ts +1 -0
- package/dist/lib/__tests__/test-utils/ast.js +4 -0
- package/dist/lib/__tests__/utils.spec.d.ts +1 -0
- package/dist/lib/__tests__/utils.spec.js +176 -0
- package/dist/lib/ast.d.ts +1 -1
- package/dist/lib/ast.js +22 -29
- package/dist/lib/bin/cli.js +52 -72
- package/dist/lib/build.d.ts +1 -1
- package/dist/lib/build.js +60 -78
- package/dist/lib/config.d.ts +1 -1
- package/dist/lib/config.js +31 -34
- package/dist/lib/constants.d.ts +0 -2
- package/dist/lib/constants.js +20 -27
- package/dist/lib/dev.d.ts +1 -1
- package/dist/lib/dev.js +52 -69
- package/dist/lib/extensionsService.d.ts +1 -1
- package/dist/lib/extensionsService.js +21 -15
- package/dist/lib/parsing-utils.d.ts +1 -1
- package/dist/lib/parsing-utils.js +7 -11
- package/dist/lib/plugins/codeBlockingPlugin.d.ts +1 -1
- package/dist/lib/plugins/codeBlockingPlugin.js +5 -8
- package/dist/lib/plugins/codeCheckingPlugin.d.ts +1 -1
- package/dist/lib/plugins/codeCheckingPlugin.js +10 -11
- package/dist/lib/plugins/devBuildPlugin.d.ts +2 -2
- package/dist/lib/plugins/devBuildPlugin.js +74 -99
- package/dist/lib/plugins/friendlyLoggingPlugin.d.ts +2 -2
- package/dist/lib/plugins/friendlyLoggingPlugin.js +4 -12
- package/dist/lib/plugins/manifestPlugin.d.ts +1 -1
- package/dist/lib/plugins/manifestPlugin.js +46 -26
- package/dist/lib/plugins/relevantModulesPlugin.d.ts +2 -2
- package/dist/lib/plugins/relevantModulesPlugin.js +4 -7
- package/dist/lib/server.d.ts +7 -2
- package/dist/lib/server.js +85 -84
- package/dist/lib/types.d.ts +1 -1
- package/dist/lib/types.js +4 -7
- package/dist/lib/utils.d.ts +1 -1
- package/dist/lib/utils.js +23 -40
- package/package.json +44 -31
|
@@ -0,0 +1,737 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import { isExtensionFileMock } from "../__mocks__/isExtensionFile.js";
|
|
3
|
+
import { traverseAbstractSyntaxTree } from "../ast.js";
|
|
4
|
+
import { generateHash } from "../utils.js";
|
|
5
|
+
import { localParse } from "./test-utils/ast.js";
|
|
6
|
+
vi.mock('../utils', async () => {
|
|
7
|
+
const originalModule = await vi.importActual('../utils');
|
|
8
|
+
return {
|
|
9
|
+
isExtensionFile: isExtensionFileMock,
|
|
10
|
+
generateHash: vi.fn().mockReturnValue('mock-hash-value'),
|
|
11
|
+
isImage: originalModule.isImage,
|
|
12
|
+
};
|
|
13
|
+
});
|
|
14
|
+
const invokedGlobalMatches = [
|
|
15
|
+
[`require('blah');`],
|
|
16
|
+
[`const foo = require('blah');`],
|
|
17
|
+
[`if (true) { require('blah');}`],
|
|
18
|
+
[`if (true) {} else { require('blah'); }`],
|
|
19
|
+
[`function bar() { require('blah') }`],
|
|
20
|
+
[`(function () {require('./module');})();`],
|
|
21
|
+
[
|
|
22
|
+
`if (false) {
|
|
23
|
+
function fdkasl() {
|
|
24
|
+
const blah = () => {
|
|
25
|
+
const blah2 = () => {
|
|
26
|
+
require('ok');
|
|
27
|
+
};
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
}`,
|
|
31
|
+
],
|
|
32
|
+
];
|
|
33
|
+
const localMethodInvocations = [
|
|
34
|
+
[
|
|
35
|
+
`const require = a => {
|
|
36
|
+
console.log('this is valid', a);
|
|
37
|
+
};
|
|
38
|
+
require('');`,
|
|
39
|
+
],
|
|
40
|
+
[
|
|
41
|
+
` function require(a) {
|
|
42
|
+
console.log('this is valid', a);
|
|
43
|
+
}
|
|
44
|
+
require('');`,
|
|
45
|
+
],
|
|
46
|
+
[
|
|
47
|
+
` import { require } from 'blah';
|
|
48
|
+
require('');`,
|
|
49
|
+
],
|
|
50
|
+
[
|
|
51
|
+
` import require from 'blah';
|
|
52
|
+
require('');`,
|
|
53
|
+
],
|
|
54
|
+
[
|
|
55
|
+
` import * as require from 'blah';
|
|
56
|
+
require('');`,
|
|
57
|
+
],
|
|
58
|
+
[
|
|
59
|
+
`function foo(require) {
|
|
60
|
+
require('blah')
|
|
61
|
+
}`,
|
|
62
|
+
],
|
|
63
|
+
[
|
|
64
|
+
`const { require } = { require: () => {}};
|
|
65
|
+
require('')`,
|
|
66
|
+
],
|
|
67
|
+
];
|
|
68
|
+
const ignoredInvocations = [[`foo.require()`]];
|
|
69
|
+
describe('ast', () => {
|
|
70
|
+
const extensionPath = '/mock/extension/path';
|
|
71
|
+
const mockLogger = {
|
|
72
|
+
debug: vi.fn(),
|
|
73
|
+
info: vi.fn(),
|
|
74
|
+
warn: vi.fn(),
|
|
75
|
+
error: vi.fn(),
|
|
76
|
+
};
|
|
77
|
+
beforeEach(() => {
|
|
78
|
+
vi.clearAllMocks();
|
|
79
|
+
});
|
|
80
|
+
describe('function checks', () => {
|
|
81
|
+
it('should not add metadata for a function that is not relevant in the source', () => {
|
|
82
|
+
const functionName = 'foo';
|
|
83
|
+
const program = localParse(`
|
|
84
|
+
const bar = () => {}
|
|
85
|
+
`);
|
|
86
|
+
const output = traverseAbstractSyntaxTree(program, [{ functionName }], extensionPath, mockLogger);
|
|
87
|
+
expect(output.functions[functionName]).toStrictEqual({});
|
|
88
|
+
});
|
|
89
|
+
it('should not add metadata for a function we are not checking for', () => {
|
|
90
|
+
const program = localParse(`
|
|
91
|
+
const bar = () => {}
|
|
92
|
+
`);
|
|
93
|
+
const output = traverseAbstractSyntaxTree(program, [], extensionPath, mockLogger);
|
|
94
|
+
expect(output.functions['bar']).toBe(undefined);
|
|
95
|
+
});
|
|
96
|
+
it.each(invokedGlobalMatches)('should flag invoked undefined methods as Global \n%s', (source) => {
|
|
97
|
+
const functionName = 'require';
|
|
98
|
+
const program = localParse(source);
|
|
99
|
+
const output = traverseAbstractSyntaxTree(program, [{ functionName }], extensionPath, mockLogger);
|
|
100
|
+
expect(output.functions[functionName]).toStrictEqual({
|
|
101
|
+
invoked: true,
|
|
102
|
+
scope: 'Global',
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
it.each(localMethodInvocations)('should flag defined invoked methods as Local \n%s', (source) => {
|
|
106
|
+
const functionName = 'require';
|
|
107
|
+
const program = localParse(source);
|
|
108
|
+
const output = traverseAbstractSyntaxTree(program, [{ functionName }], extensionPath, mockLogger);
|
|
109
|
+
expect(output.functions[functionName]).toStrictEqual({
|
|
110
|
+
invoked: true,
|
|
111
|
+
scope: 'Local',
|
|
112
|
+
defined: true,
|
|
113
|
+
});
|
|
114
|
+
});
|
|
115
|
+
it.each(ignoredInvocations)('should not gather metadata on irrelevant expressions \n%s', (source) => {
|
|
116
|
+
const functionName = 'require';
|
|
117
|
+
const program = localParse(source);
|
|
118
|
+
const output = traverseAbstractSyntaxTree(program, [{ functionName }], extensionPath, mockLogger);
|
|
119
|
+
expect(output.functions[functionName]).toStrictEqual({});
|
|
120
|
+
});
|
|
121
|
+
['png', 'jpg', 'jpeg', 'gif', 'svg', 'webp', 'avif'].forEach((ext) => {
|
|
122
|
+
it(`Should correctly identify imports inside and outside the extension path for ${ext} type`, () => {
|
|
123
|
+
const program = localParse(`
|
|
124
|
+
import moduleA from './assets/local-image.${ext}';
|
|
125
|
+
import moduleB from '../assets/external-image.${ext}';
|
|
126
|
+
`);
|
|
127
|
+
const output = traverseAbstractSyntaxTree(program, [], extensionPath, mockLogger);
|
|
128
|
+
expect(output.badImports).toContain(`../assets/external-image.${ext}`);
|
|
129
|
+
expect(output.badImports).not.toContain(`./assets/local-image.${ext}`);
|
|
130
|
+
});
|
|
131
|
+
});
|
|
132
|
+
it('should not flag non-image imports', () => {
|
|
133
|
+
const program = localParse(`
|
|
134
|
+
import moduleA from '../utils/file.js';
|
|
135
|
+
import moduleB from './utils/file.js';
|
|
136
|
+
import moduleC from '@package/external-file.js';
|
|
137
|
+
`);
|
|
138
|
+
const output = traverseAbstractSyntaxTree(program, [], extensionPath, mockLogger);
|
|
139
|
+
expect(output.badImports).not.toContain('../utils/file.js');
|
|
140
|
+
expect(output.badImports).not.toContain('./utils/file.js');
|
|
141
|
+
expect(output.badImports).not.toContain('@package/external-file.js');
|
|
142
|
+
});
|
|
143
|
+
describe('data dependencies', () => {
|
|
144
|
+
it('should collect CRM properties from useCrmProperties hook', () => {
|
|
145
|
+
const source = `
|
|
146
|
+
import { useCrmProperties } from '@hubspot/ui-extensions/crm';
|
|
147
|
+
function MyComponent() {
|
|
148
|
+
const properties = useCrmProperties(['firstname', 'lastname']);
|
|
149
|
+
}
|
|
150
|
+
`;
|
|
151
|
+
const program = localParse(source);
|
|
152
|
+
const output = traverseAbstractSyntaxTree(program, [], extensionPath, mockLogger);
|
|
153
|
+
expect(output.dataDependencies.importedHooks).toHaveProperty('useCrmProperties', 'useCrmProperties');
|
|
154
|
+
expect(output.dataDependencies.dependencies).toHaveLength(1);
|
|
155
|
+
expect(output.dataDependencies.dependencies[0]).toEqual({
|
|
156
|
+
referenceId: 'mock-hash-value',
|
|
157
|
+
properties: {
|
|
158
|
+
type: 'CrmRecordProperties',
|
|
159
|
+
recordProperties: ['firstname', 'lastname'],
|
|
160
|
+
options: {},
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
expect(generateHash).toHaveBeenCalledWith('CrmRecordProperties', [
|
|
164
|
+
'firstname',
|
|
165
|
+
'lastname',
|
|
166
|
+
]);
|
|
167
|
+
});
|
|
168
|
+
it('should handle useCrmProperties with no properties', () => {
|
|
169
|
+
const source = `
|
|
170
|
+
import { useCrmProperties } from '@hubspot/ui-extensions/crm';
|
|
171
|
+
function MyComponent() {
|
|
172
|
+
const properties = useCrmProperties([]);
|
|
173
|
+
}
|
|
174
|
+
`;
|
|
175
|
+
const program = localParse(source);
|
|
176
|
+
const output = traverseAbstractSyntaxTree(program, [], extensionPath, mockLogger);
|
|
177
|
+
expect(output.dataDependencies.importedHooks).toHaveProperty('useCrmProperties', 'useCrmProperties');
|
|
178
|
+
expect(output.dataDependencies.dependencies).toHaveLength(0);
|
|
179
|
+
});
|
|
180
|
+
it('should handle multiple useCrmProperties calls', () => {
|
|
181
|
+
const source = `
|
|
182
|
+
import { useCrmProperties } from '@hubspot/ui-extensions/crm';
|
|
183
|
+
function MyComponent() {
|
|
184
|
+
useCrmProperties(['firstname']);
|
|
185
|
+
useCrmProperties(['lastname']);
|
|
186
|
+
}
|
|
187
|
+
`;
|
|
188
|
+
const program = localParse(source);
|
|
189
|
+
const output = traverseAbstractSyntaxTree(program, [], extensionPath, mockLogger);
|
|
190
|
+
expect(output.dataDependencies.importedHooks).toHaveProperty('useCrmProperties', 'useCrmProperties');
|
|
191
|
+
expect(output.dataDependencies.dependencies).toHaveLength(2);
|
|
192
|
+
const dep0 = output.dataDependencies.dependencies[0];
|
|
193
|
+
const dep1 = output.dataDependencies.dependencies[1];
|
|
194
|
+
expect(dep0.properties.type).toBe('CrmRecordProperties');
|
|
195
|
+
expect(dep1.properties.type).toBe('CrmRecordProperties');
|
|
196
|
+
if (dep0.properties.type === 'CrmRecordProperties') {
|
|
197
|
+
expect(dep0.properties.recordProperties).toEqual(['firstname']);
|
|
198
|
+
}
|
|
199
|
+
if (dep1.properties.type === 'CrmRecordProperties') {
|
|
200
|
+
expect(dep1.properties.recordProperties).toEqual(['lastname']);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
it('should handle non-string property names gracefully', () => {
|
|
204
|
+
const source = `
|
|
205
|
+
import { useCrmProperties } from '@hubspot/ui-extensions/crm';
|
|
206
|
+
function MyComponent() {
|
|
207
|
+
useCrmProperties(['firstname', 123, null]);
|
|
208
|
+
}
|
|
209
|
+
`;
|
|
210
|
+
const program = localParse(source);
|
|
211
|
+
const output = traverseAbstractSyntaxTree(program, [], extensionPath, mockLogger);
|
|
212
|
+
expect(output.dataDependencies.importedHooks).toHaveProperty('useCrmProperties', 'useCrmProperties');
|
|
213
|
+
expect(output.dataDependencies.dependencies).toHaveLength(1);
|
|
214
|
+
const dep = output.dataDependencies.dependencies[0];
|
|
215
|
+
expect(dep.properties.type).toBe('CrmRecordProperties');
|
|
216
|
+
if (dep.properties.type === 'CrmRecordProperties') {
|
|
217
|
+
expect(dep.properties.recordProperties).toEqual(['firstname']);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
it('should handle errors in data dependency collection', () => {
|
|
221
|
+
const source = `
|
|
222
|
+
import { useCrmProperties } from '@hubspot/ui-extensions/crm';
|
|
223
|
+
function MyComponent() {
|
|
224
|
+
useCrmProperties(['firstname']);
|
|
225
|
+
}
|
|
226
|
+
`;
|
|
227
|
+
const program = localParse(source);
|
|
228
|
+
// Mock generateHash to throw an error
|
|
229
|
+
vi.mocked(generateHash).mockImplementationOnce(() => {
|
|
230
|
+
throw new Error('Hash generation failed');
|
|
231
|
+
});
|
|
232
|
+
const output = traverseAbstractSyntaxTree(program, [], extensionPath, mockLogger);
|
|
233
|
+
expect(output.dataDependencies.importedHooks).toHaveProperty('useCrmProperties', 'useCrmProperties');
|
|
234
|
+
expect(output.dataDependencies.dependencies).toEqual([]);
|
|
235
|
+
expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining('Error collecting data dependencies (skipping)'));
|
|
236
|
+
});
|
|
237
|
+
it('should handle malformed AST nodes gracefully', () => {
|
|
238
|
+
const source = `
|
|
239
|
+
import { useCrmProperties } from '@hubspot/ui-extensions/crm';
|
|
240
|
+
function MyComponent() {
|
|
241
|
+
useCrmProperties(); // Missing arguments
|
|
242
|
+
}
|
|
243
|
+
`;
|
|
244
|
+
const program = localParse(source);
|
|
245
|
+
const output = traverseAbstractSyntaxTree(program, [], extensionPath, mockLogger);
|
|
246
|
+
expect(output.dataDependencies.importedHooks).toHaveProperty('useCrmProperties', 'useCrmProperties');
|
|
247
|
+
expect(output.dataDependencies.dependencies).toEqual([]);
|
|
248
|
+
});
|
|
249
|
+
it('should not collect dependencies if hook is not imported', () => {
|
|
250
|
+
const source = `
|
|
251
|
+
function MyComponent() {
|
|
252
|
+
useCrmProperties(['firstname']);
|
|
253
|
+
}
|
|
254
|
+
`;
|
|
255
|
+
const program = localParse(source);
|
|
256
|
+
const output = traverseAbstractSyntaxTree(program, [], extensionPath, mockLogger);
|
|
257
|
+
expect(output.dataDependencies.importedHooks).not.toHaveProperty('useCrmProperties');
|
|
258
|
+
expect(output.dataDependencies.dependencies).toEqual([]);
|
|
259
|
+
});
|
|
260
|
+
it('should not collect dependencies if useCrmProperties is defined locally', () => {
|
|
261
|
+
const source = `
|
|
262
|
+
// Define a local function with the same name
|
|
263
|
+
function useCrmProperties(properties) {
|
|
264
|
+
return properties.map(p => ({ [p]: 'custom value' }));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function MyComponent() {
|
|
268
|
+
// This should not be treated as a HubSpot hook call
|
|
269
|
+
const result = useCrmProperties(['firstname', 'lastname']);
|
|
270
|
+
}
|
|
271
|
+
`;
|
|
272
|
+
const program = localParse(source);
|
|
273
|
+
const output = traverseAbstractSyntaxTree(program, [], extensionPath, mockLogger);
|
|
274
|
+
expect(output.dataDependencies.importedHooks).not.toHaveProperty('useCrmProperties');
|
|
275
|
+
expect(output.dataDependencies.dependencies).toEqual([]);
|
|
276
|
+
});
|
|
277
|
+
it('should handle namespace imports of useCrmProperties', () => {
|
|
278
|
+
const source = `
|
|
279
|
+
import * as hsCrm from '@hubspot/ui-extensions/crm';
|
|
280
|
+
function MyComponent() {
|
|
281
|
+
hsCrm.useCrmProperties(['firstname', 'lastname']);
|
|
282
|
+
}
|
|
283
|
+
`;
|
|
284
|
+
const program = localParse(source);
|
|
285
|
+
const output = traverseAbstractSyntaxTree(program, [], extensionPath, mockLogger);
|
|
286
|
+
expect(output.dataDependencies.importedHooks['hsCrm.useCrmProperties']).toBe('useCrmProperties');
|
|
287
|
+
expect(output.dataDependencies.dependencies).toHaveLength(1);
|
|
288
|
+
expect(output.dataDependencies.dependencies[0]).toEqual({
|
|
289
|
+
referenceId: 'mock-hash-value',
|
|
290
|
+
properties: {
|
|
291
|
+
type: 'CrmRecordProperties',
|
|
292
|
+
recordProperties: ['firstname', 'lastname'],
|
|
293
|
+
options: {},
|
|
294
|
+
},
|
|
295
|
+
});
|
|
296
|
+
expect(generateHash).toHaveBeenCalledWith('CrmRecordProperties', [
|
|
297
|
+
'firstname',
|
|
298
|
+
'lastname',
|
|
299
|
+
]);
|
|
300
|
+
});
|
|
301
|
+
it('should handle aliased imports of useCrmProperties', () => {
|
|
302
|
+
const source = `
|
|
303
|
+
import { useCrmProperties as anotherHookName } from '@hubspot/ui-extensions/crm';
|
|
304
|
+
function MyComponent() {
|
|
305
|
+
anotherHookName(['firstname', 'lastname']);
|
|
306
|
+
}
|
|
307
|
+
`;
|
|
308
|
+
const program = localParse(source);
|
|
309
|
+
const output = traverseAbstractSyntaxTree(program, [], extensionPath, mockLogger);
|
|
310
|
+
expect(output.dataDependencies.importedHooks).toHaveProperty('anotherHookName', 'useCrmProperties');
|
|
311
|
+
expect(output.dataDependencies.dependencies).toHaveLength(1);
|
|
312
|
+
expect(output.dataDependencies.dependencies[0]).toEqual({
|
|
313
|
+
referenceId: 'mock-hash-value',
|
|
314
|
+
properties: {
|
|
315
|
+
type: 'CrmRecordProperties',
|
|
316
|
+
recordProperties: ['firstname', 'lastname'],
|
|
317
|
+
options: {},
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
expect(generateHash).toHaveBeenCalledWith('CrmRecordProperties', [
|
|
321
|
+
'firstname',
|
|
322
|
+
'lastname',
|
|
323
|
+
]);
|
|
324
|
+
});
|
|
325
|
+
it('should handle useCrmProperties with formatting options', () => {
|
|
326
|
+
const source = `
|
|
327
|
+
import { useCrmProperties } from '@hubspot/ui-extensions/crm';
|
|
328
|
+
function MyComponent() {
|
|
329
|
+
const properties = useCrmProperties(
|
|
330
|
+
['firstname', 'lastname', 'dealamount', 'telephone'],
|
|
331
|
+
{ propertiesToFormat: ['dealamount', 'telephone'], formattingOptions: { currency: { addSymbol: true } } }
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
`;
|
|
335
|
+
const program = localParse(source);
|
|
336
|
+
const output = traverseAbstractSyntaxTree(program, [], extensionPath, mockLogger);
|
|
337
|
+
expect(output.dataDependencies.importedHooks).toHaveProperty('useCrmProperties', 'useCrmProperties');
|
|
338
|
+
expect(output.dataDependencies.dependencies).toHaveLength(1);
|
|
339
|
+
expect(output.dataDependencies.dependencies[0]).toEqual({
|
|
340
|
+
referenceId: 'mock-hash-value',
|
|
341
|
+
properties: {
|
|
342
|
+
type: 'CrmRecordProperties',
|
|
343
|
+
recordProperties: [
|
|
344
|
+
'firstname',
|
|
345
|
+
'lastname',
|
|
346
|
+
'dealamount',
|
|
347
|
+
'telephone',
|
|
348
|
+
],
|
|
349
|
+
options: {
|
|
350
|
+
propertiesToFormat: ['dealamount', 'telephone'],
|
|
351
|
+
formattingOptions: {
|
|
352
|
+
currency: { addSymbol: true },
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
});
|
|
357
|
+
expect(generateHash).toHaveBeenCalledWith('CrmRecordProperties', [
|
|
358
|
+
'firstname',
|
|
359
|
+
'lastname',
|
|
360
|
+
'dealamount',
|
|
361
|
+
'telephone',
|
|
362
|
+
]);
|
|
363
|
+
});
|
|
364
|
+
it('should resolve variables in useCrmProperties calls', () => {
|
|
365
|
+
const source = `
|
|
366
|
+
import { useCrmProperties } from '@hubspot/ui-extensions/crm';
|
|
367
|
+
|
|
368
|
+
const CONTACT_PROPERTIES = ['firstname', 'lastname', 'email'];
|
|
369
|
+
const DEAL_PROPERTIES = ['dealname', 'amount', 'stage'];
|
|
370
|
+
const contactOptions = { formatFields: ['email'] };
|
|
371
|
+
const dealOptions = { formatFields: ['amount'], currency: 'USD' };
|
|
372
|
+
|
|
373
|
+
function MyComponent() {
|
|
374
|
+
const { properties: contactProps } = useCrmProperties(CONTACT_PROPERTIES, { formatFields: 'all' });
|
|
375
|
+
const { properties: dealProps } = useCrmProperties(['closedate', 'probability'], dealOptions);
|
|
376
|
+
const { properties: mixedProps } = useCrmProperties(DEAL_PROPERTIES, contactOptions);
|
|
377
|
+
}
|
|
378
|
+
`;
|
|
379
|
+
const program = localParse(source);
|
|
380
|
+
const output = traverseAbstractSyntaxTree(program, [], extensionPath, mockLogger);
|
|
381
|
+
expect(output.dataDependencies.dependencies).toHaveLength(3);
|
|
382
|
+
expect(output.dataDependencies.dependencies[0]).toEqual({
|
|
383
|
+
referenceId: 'mock-hash-value',
|
|
384
|
+
properties: {
|
|
385
|
+
type: 'CrmRecordProperties',
|
|
386
|
+
recordProperties: ['firstname', 'lastname', 'email'],
|
|
387
|
+
options: {
|
|
388
|
+
formatFields: 'all',
|
|
389
|
+
},
|
|
390
|
+
},
|
|
391
|
+
});
|
|
392
|
+
expect(output.dataDependencies.dependencies[1]).toEqual({
|
|
393
|
+
referenceId: 'mock-hash-value',
|
|
394
|
+
properties: {
|
|
395
|
+
type: 'CrmRecordProperties',
|
|
396
|
+
recordProperties: ['closedate', 'probability'],
|
|
397
|
+
options: {
|
|
398
|
+
formatFields: ['amount'],
|
|
399
|
+
currency: 'USD',
|
|
400
|
+
},
|
|
401
|
+
},
|
|
402
|
+
});
|
|
403
|
+
expect(output.dataDependencies.dependencies[2]).toEqual({
|
|
404
|
+
referenceId: 'mock-hash-value',
|
|
405
|
+
properties: {
|
|
406
|
+
type: 'CrmRecordProperties',
|
|
407
|
+
recordProperties: ['dealname', 'amount', 'stage'],
|
|
408
|
+
options: {
|
|
409
|
+
formatFields: ['email'],
|
|
410
|
+
},
|
|
411
|
+
},
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
it('should handle supported spread operators in useCrmProperties calls', () => {
|
|
415
|
+
const source = `
|
|
416
|
+
import { useCrmProperties } from '@hubspot/ui-extensions/crm';
|
|
417
|
+
|
|
418
|
+
const propsToFetch = ['firstname', 'lastname', 'email'];
|
|
419
|
+
const propsPartTwo = ['phone', 'company'];
|
|
420
|
+
const subOptions = {
|
|
421
|
+
date: {
|
|
422
|
+
relative: true,
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
// Unsupported spread operator
|
|
426
|
+
const propsFunction = () => ['createdate'];
|
|
427
|
+
|
|
428
|
+
const fieldsFormat = {
|
|
429
|
+
fieldsToFormat: ['firstname', 'lastname', 'email', 'phone'],
|
|
430
|
+
};
|
|
431
|
+
|
|
432
|
+
const options = {
|
|
433
|
+
...fieldsFormat,
|
|
434
|
+
formattingOptions: {
|
|
435
|
+
date: {
|
|
436
|
+
relative: false,
|
|
437
|
+
},
|
|
438
|
+
...{dateTime: { relative: true } },
|
|
439
|
+
...subOptions
|
|
440
|
+
}
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
const { properties } = useCrmProperties([...propsToFetch, ...propsPartTwo], options);
|
|
445
|
+
`;
|
|
446
|
+
const program = localParse(source);
|
|
447
|
+
const output = traverseAbstractSyntaxTree(program, [], extensionPath, mockLogger);
|
|
448
|
+
expect(output.dataDependencies.dependencies).toHaveLength(1);
|
|
449
|
+
expect(output.dataDependencies.dependencies[0]).toEqual({
|
|
450
|
+
referenceId: 'mock-hash-value',
|
|
451
|
+
properties: {
|
|
452
|
+
type: 'CrmRecordProperties',
|
|
453
|
+
recordProperties: [
|
|
454
|
+
'firstname',
|
|
455
|
+
'lastname',
|
|
456
|
+
'email',
|
|
457
|
+
'phone',
|
|
458
|
+
'company',
|
|
459
|
+
],
|
|
460
|
+
options: {
|
|
461
|
+
fieldsToFormat: ['firstname', 'lastname', 'email', 'phone'],
|
|
462
|
+
formattingOptions: {
|
|
463
|
+
date: {
|
|
464
|
+
relative: true,
|
|
465
|
+
},
|
|
466
|
+
dateTime: {
|
|
467
|
+
relative: true,
|
|
468
|
+
},
|
|
469
|
+
},
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
});
|
|
473
|
+
});
|
|
474
|
+
it('should not create data dependencies when unsupported spread operators are used', () => {
|
|
475
|
+
const source = `
|
|
476
|
+
import { useCrmProperties } from '@hubspot/ui-extensions/crm';
|
|
477
|
+
|
|
478
|
+
const propsToFetch = ['firstname', 'lastname'];
|
|
479
|
+
const propsFunction = () => ['email'];
|
|
480
|
+
|
|
481
|
+
const { properties } = useCrmProperties([...propsToFetch, ...propsFunction()]);
|
|
482
|
+
`;
|
|
483
|
+
const program = localParse(source);
|
|
484
|
+
const output = traverseAbstractSyntaxTree(program, [], extensionPath, mockLogger);
|
|
485
|
+
// Should not create any dependencies because parsing failed
|
|
486
|
+
expect(output.dataDependencies.dependencies).toHaveLength(0);
|
|
487
|
+
});
|
|
488
|
+
it('should handle template literals in useCrmProperties calls', () => {
|
|
489
|
+
const source = `
|
|
490
|
+
import { useCrmProperties } from '@hubspot/ui-extensions/crm';
|
|
491
|
+
|
|
492
|
+
const prefix = 'contact';
|
|
493
|
+
const suffix = 'name';
|
|
494
|
+
const config = { type: 'user', nested: { field: 'email' } };
|
|
495
|
+
|
|
496
|
+
function MyComponent() {
|
|
497
|
+
const { properties: test1 } = useCrmProperties([\`\${prefix}_\${suffix}\`]);
|
|
498
|
+
const { properties: test2 } = useCrmProperties([\`Hello \${prefix}\`]);
|
|
499
|
+
const { properties: test3 } = useCrmProperties([\`\${config.type}_id\`]);
|
|
500
|
+
const { properties: test4 } = useCrmProperties([\`\${config.nested.field}_formatted\`]);
|
|
501
|
+
const { properties: test5 } = useCrmProperties([\`static_prop\`, \`\${prefix}_dynamic\`]);
|
|
502
|
+
}
|
|
503
|
+
`;
|
|
504
|
+
const program = localParse(source);
|
|
505
|
+
const output = traverseAbstractSyntaxTree(program, [], extensionPath, mockLogger);
|
|
506
|
+
expect(output.dataDependencies.dependencies).toHaveLength(5);
|
|
507
|
+
output.dataDependencies.dependencies.forEach((dep) => {
|
|
508
|
+
expect(dep.properties.type).toBe('CrmRecordProperties');
|
|
509
|
+
});
|
|
510
|
+
const deps = output.dataDependencies.dependencies;
|
|
511
|
+
if (deps[0].properties.type === 'CrmRecordProperties') {
|
|
512
|
+
expect(deps[0].properties.recordProperties).toEqual(['contact_name']);
|
|
513
|
+
}
|
|
514
|
+
if (deps[1].properties.type === 'CrmRecordProperties') {
|
|
515
|
+
expect(deps[1].properties.recordProperties).toEqual([
|
|
516
|
+
'Hello contact',
|
|
517
|
+
]);
|
|
518
|
+
}
|
|
519
|
+
if (deps[2].properties.type === 'CrmRecordProperties') {
|
|
520
|
+
expect(deps[2].properties.recordProperties).toEqual(['user_id']);
|
|
521
|
+
}
|
|
522
|
+
if (deps[3].properties.type === 'CrmRecordProperties') {
|
|
523
|
+
expect(deps[3].properties.recordProperties).toEqual([
|
|
524
|
+
'email_formatted',
|
|
525
|
+
]);
|
|
526
|
+
}
|
|
527
|
+
if (deps[4].properties.type === 'CrmRecordProperties') {
|
|
528
|
+
expect(deps[4].properties.recordProperties).toEqual([
|
|
529
|
+
'static_prop',
|
|
530
|
+
'contact_dynamic',
|
|
531
|
+
]);
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
});
|
|
535
|
+
});
|
|
536
|
+
describe('variable collection', () => {
|
|
537
|
+
it('should collect all types of variable declarations', () => {
|
|
538
|
+
const source = `
|
|
539
|
+
const CONTACT_PROPERTIES = ['firstname', 'lastname', 'email'];
|
|
540
|
+
let USER_COUNT = 42;
|
|
541
|
+
var IS_ENABLED = true;
|
|
542
|
+
const CONFIG = { formatFields: ['email'] };
|
|
543
|
+
`;
|
|
544
|
+
const program = localParse(source);
|
|
545
|
+
const output = traverseAbstractSyntaxTree(program, [], extensionPath, mockLogger);
|
|
546
|
+
expect(output.variableDeclarations.get('CONTACT_PROPERTIES')).toEqual([
|
|
547
|
+
'firstname',
|
|
548
|
+
'lastname',
|
|
549
|
+
'email',
|
|
550
|
+
]);
|
|
551
|
+
expect(output.variableDeclarations.get('USER_COUNT')).toBe(42);
|
|
552
|
+
expect(output.variableDeclarations.get('IS_ENABLED')).toBe(true);
|
|
553
|
+
expect(output.variableDeclarations.get('CONFIG')).toEqual({
|
|
554
|
+
formatFields: ['email'],
|
|
555
|
+
});
|
|
556
|
+
});
|
|
557
|
+
it('should track variable updates for let variables', () => {
|
|
558
|
+
const source = `
|
|
559
|
+
let PROPERTIES = ['firstname', 'lastname'];
|
|
560
|
+
PROPERTIES = ['firstname', 'lastname', 'email'];
|
|
561
|
+
`;
|
|
562
|
+
const program = localParse(source);
|
|
563
|
+
const output = traverseAbstractSyntaxTree(program, [], extensionPath, mockLogger);
|
|
564
|
+
// Should have the updated value, not the original
|
|
565
|
+
expect(output.variableDeclarations.get('PROPERTIES')).toEqual([
|
|
566
|
+
'firstname',
|
|
567
|
+
'lastname',
|
|
568
|
+
'email',
|
|
569
|
+
]);
|
|
570
|
+
});
|
|
571
|
+
it('should handle multiple variable declarations in one statement', () => {
|
|
572
|
+
const source = `
|
|
573
|
+
const FIRST = ['a', 'b'], SECOND = { test: true }, THIRD = 'hello';
|
|
574
|
+
`;
|
|
575
|
+
const program = localParse(source);
|
|
576
|
+
const output = traverseAbstractSyntaxTree(program, [], extensionPath, mockLogger);
|
|
577
|
+
expect(output.variableDeclarations.get('FIRST')).toEqual(['a', 'b']);
|
|
578
|
+
expect(output.variableDeclarations.get('SECOND')).toEqual({ test: true });
|
|
579
|
+
expect(output.variableDeclarations.get('THIRD')).toBe('hello');
|
|
580
|
+
});
|
|
581
|
+
});
|
|
582
|
+
describe('useAssociations hook parsing', () => {
|
|
583
|
+
it('should parse basic useAssociations call', () => {
|
|
584
|
+
const source = `
|
|
585
|
+
import { useAssociations } from '@hubspot/ui-extensions/crm';
|
|
586
|
+
const result = useAssociations({
|
|
587
|
+
toObjectType: '0-1',
|
|
588
|
+
properties: ['firstname', 'lastname']
|
|
589
|
+
});
|
|
590
|
+
`;
|
|
591
|
+
const program = localParse(source);
|
|
592
|
+
const output = traverseAbstractSyntaxTree(program, [], extensionPath, mockLogger);
|
|
593
|
+
expect(output.dataDependencies.dependencies).toHaveLength(1);
|
|
594
|
+
const dep = output.dataDependencies.dependencies[0];
|
|
595
|
+
expect(dep.properties.type).toBe('CrmRecordAssociationProperties');
|
|
596
|
+
if (dep.properties.type === 'CrmRecordAssociationProperties') {
|
|
597
|
+
expect(dep.properties.toObjectTypeId).toBe('0-1');
|
|
598
|
+
expect(dep.properties.requestProperties).toEqual([
|
|
599
|
+
'firstname',
|
|
600
|
+
'lastname',
|
|
601
|
+
]);
|
|
602
|
+
expect(dep.properties.paginationOptions).toEqual({});
|
|
603
|
+
expect(dep.properties.options).toEqual({});
|
|
604
|
+
}
|
|
605
|
+
});
|
|
606
|
+
it('should parse useAssociations with pagination options', () => {
|
|
607
|
+
const source = `
|
|
608
|
+
import { useAssociations } from '@hubspot/ui-extensions/crm';
|
|
609
|
+
const result = useAssociations({
|
|
610
|
+
toObjectType: '0-2',
|
|
611
|
+
properties: ['name', 'domain'],
|
|
612
|
+
pageLength: 25,
|
|
613
|
+
offset: 50
|
|
614
|
+
});
|
|
615
|
+
`;
|
|
616
|
+
const program = localParse(source);
|
|
617
|
+
const output = traverseAbstractSyntaxTree(program, [], extensionPath, mockLogger);
|
|
618
|
+
expect(output.dataDependencies.dependencies).toHaveLength(1);
|
|
619
|
+
const dep = output.dataDependencies.dependencies[0];
|
|
620
|
+
expect(dep.properties.type).toBe('CrmRecordAssociationProperties');
|
|
621
|
+
if (dep.properties.type === 'CrmRecordAssociationProperties') {
|
|
622
|
+
expect(dep.properties.toObjectTypeId).toBe('0-2');
|
|
623
|
+
expect(dep.properties.requestProperties).toEqual(['name', 'domain']);
|
|
624
|
+
expect(dep.properties.paginationOptions).toEqual({
|
|
625
|
+
pageLength: 25,
|
|
626
|
+
offset: 50,
|
|
627
|
+
});
|
|
628
|
+
}
|
|
629
|
+
});
|
|
630
|
+
it('should parse useAssociations with options', () => {
|
|
631
|
+
const source = `
|
|
632
|
+
import { useAssociations } from '@hubspot/ui-extensions/crm';
|
|
633
|
+
const result = useAssociations(
|
|
634
|
+
{
|
|
635
|
+
toObjectType: '0-3',
|
|
636
|
+
properties: ['dealname', 'amount']
|
|
637
|
+
},
|
|
638
|
+
{
|
|
639
|
+
propertiesToFormat: 'all',
|
|
640
|
+
formattingOptions: { currency: { addSymbol: true } }
|
|
641
|
+
}
|
|
642
|
+
);
|
|
643
|
+
`;
|
|
644
|
+
const program = localParse(source);
|
|
645
|
+
const output = traverseAbstractSyntaxTree(program, [], extensionPath, mockLogger);
|
|
646
|
+
expect(output.dataDependencies.dependencies).toHaveLength(1);
|
|
647
|
+
const dep = output.dataDependencies.dependencies[0];
|
|
648
|
+
expect(dep.properties.type).toBe('CrmRecordAssociationProperties');
|
|
649
|
+
if (dep.properties.type === 'CrmRecordAssociationProperties') {
|
|
650
|
+
expect(dep.properties.toObjectTypeId).toBe('0-3');
|
|
651
|
+
expect(dep.properties.requestProperties).toEqual([
|
|
652
|
+
'dealname',
|
|
653
|
+
'amount',
|
|
654
|
+
]);
|
|
655
|
+
expect(dep.properties.options).toEqual({
|
|
656
|
+
propertiesToFormat: 'all',
|
|
657
|
+
formattingOptions: { currency: { addSymbol: true } },
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
it('should parse useAssociations with variable request', () => {
|
|
662
|
+
const source = `
|
|
663
|
+
import { useAssociations } from '@hubspot/ui-extensions/crm';
|
|
664
|
+
const request = {
|
|
665
|
+
toObjectType: '0-1',
|
|
666
|
+
properties: ['firstname', 'email'],
|
|
667
|
+
pageLength: 10
|
|
668
|
+
};
|
|
669
|
+
const result = useAssociations(request);
|
|
670
|
+
`;
|
|
671
|
+
const program = localParse(source);
|
|
672
|
+
const output = traverseAbstractSyntaxTree(program, [], extensionPath, mockLogger);
|
|
673
|
+
expect(output.dataDependencies.dependencies).toHaveLength(1);
|
|
674
|
+
const dep = output.dataDependencies.dependencies[0];
|
|
675
|
+
expect(dep.properties.type).toBe('CrmRecordAssociationProperties');
|
|
676
|
+
if (dep.properties.type === 'CrmRecordAssociationProperties') {
|
|
677
|
+
expect(dep.properties.toObjectTypeId).toBe('0-1');
|
|
678
|
+
expect(dep.properties.requestProperties).toEqual([
|
|
679
|
+
'firstname',
|
|
680
|
+
'email',
|
|
681
|
+
]);
|
|
682
|
+
expect(dep.properties.paginationOptions).toEqual({ pageLength: 10 });
|
|
683
|
+
}
|
|
684
|
+
});
|
|
685
|
+
it('should handle namespace import for useAssociations', () => {
|
|
686
|
+
const source = `
|
|
687
|
+
import * as uiExtensions from '@hubspot/ui-extensions/crm';
|
|
688
|
+
const result = uiExtensions.useAssociations({
|
|
689
|
+
toObjectType: '0-2',
|
|
690
|
+
properties: ['name']
|
|
691
|
+
});
|
|
692
|
+
`;
|
|
693
|
+
const program = localParse(source);
|
|
694
|
+
const output = traverseAbstractSyntaxTree(program, [], extensionPath, mockLogger);
|
|
695
|
+
expect(output.dataDependencies.dependencies).toHaveLength(1);
|
|
696
|
+
const dep = output.dataDependencies.dependencies[0];
|
|
697
|
+
expect(dep.properties.type).toBe('CrmRecordAssociationProperties');
|
|
698
|
+
if (dep.properties.type === 'CrmRecordAssociationProperties') {
|
|
699
|
+
expect(dep.properties.toObjectTypeId).toBe('0-2');
|
|
700
|
+
expect(dep.properties.requestProperties).toEqual(['name']);
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
it('should handle aliased import for useAssociations', () => {
|
|
704
|
+
const source = `
|
|
705
|
+
import { useAssociations as useContactAssociations } from '@hubspot/ui-extensions/crm';
|
|
706
|
+
const result = useContactAssociations({
|
|
707
|
+
toObjectType: '0-1',
|
|
708
|
+
properties: ['firstname']
|
|
709
|
+
});
|
|
710
|
+
`;
|
|
711
|
+
const program = localParse(source);
|
|
712
|
+
const output = traverseAbstractSyntaxTree(program, [], extensionPath, mockLogger);
|
|
713
|
+
expect(output.dataDependencies.dependencies).toHaveLength(1);
|
|
714
|
+
const dep = output.dataDependencies.dependencies[0];
|
|
715
|
+
expect(dep.properties.type).toBe('CrmRecordAssociationProperties');
|
|
716
|
+
if (dep.properties.type === 'CrmRecordAssociationProperties') {
|
|
717
|
+
expect(dep.properties.toObjectTypeId).toBe('0-1');
|
|
718
|
+
expect(dep.properties.requestProperties).toEqual(['firstname']);
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
it('should generate correct reference ID for useAssociations', () => {
|
|
722
|
+
const source = `
|
|
723
|
+
import { useAssociations } from '@hubspot/ui-extensions/crm';
|
|
724
|
+
const result = useAssociations({
|
|
725
|
+
toObjectType: '0-1',
|
|
726
|
+
properties: ['lastname', 'firstname'] // Note: different order
|
|
727
|
+
});
|
|
728
|
+
`;
|
|
729
|
+
const program = localParse(source);
|
|
730
|
+
const output = traverseAbstractSyntaxTree(program, [], extensionPath, mockLogger);
|
|
731
|
+
expect(output.dataDependencies.dependencies).toHaveLength(1);
|
|
732
|
+
const dep = output.dataDependencies.dependencies[0];
|
|
733
|
+
const expectedReferenceId = generateHash('CrmRecordAssociationProperties', '0-1', 'firstname-lastname');
|
|
734
|
+
expect(dep.referenceId).toBe(expectedReferenceId);
|
|
735
|
+
});
|
|
736
|
+
});
|
|
737
|
+
});
|