fg-devkit 1.17.3 → 1.19.2
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/.cursor/rules/core-coding-standards.mdc +24 -0
- package/README.md +25 -0
- package/configs/dependency-policy.vue-workspace.json +30 -30
- package/package.json +17 -14
- package/scaffold/vue/package.json +12 -12
- package/scripts/fg-devkit.mjs +154 -2
- package/scripts/piwik-common.mjs +653 -0
- package/scripts/piwik-dimensions.mjs +785 -0
- package/scripts/piwik-dimensions.test.mjs +400 -0
- package/scripts/piwik.mjs +502 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import assert from 'node:assert/strict'
|
|
2
|
+
import fs from 'node:fs'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import test from 'node:test'
|
|
5
|
+
import { fileURLToPath } from 'node:url'
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
accountFromPiwikUrl,
|
|
9
|
+
defaultEnvironmentPair,
|
|
10
|
+
defaultPiwikEnvironment,
|
|
11
|
+
discoverPiwikSites,
|
|
12
|
+
normalizeDimension,
|
|
13
|
+
uniquePiwikEnvironments,
|
|
14
|
+
} from './piwik-common.mjs'
|
|
15
|
+
import {
|
|
16
|
+
diffDimensions,
|
|
17
|
+
diffEnvironments,
|
|
18
|
+
formatDimensionReport,
|
|
19
|
+
formatEnvironmentReport,
|
|
20
|
+
parseDimensionSource,
|
|
21
|
+
planCreates,
|
|
22
|
+
planEnvironmentMigration,
|
|
23
|
+
planLocalPull,
|
|
24
|
+
rewriteDimensionNumbers,
|
|
25
|
+
} from './piwik-dimensions.mjs'
|
|
26
|
+
|
|
27
|
+
const scriptDir = path.dirname(fileURLToPath(import.meta.url))
|
|
28
|
+
|
|
29
|
+
const bookingSource = `
|
|
30
|
+
export const customDimensions = {
|
|
31
|
+
clientNumber: 1,
|
|
32
|
+
rowsBooked: 7,
|
|
33
|
+
rowsFailed: 13,
|
|
34
|
+
} as const
|
|
35
|
+
`
|
|
36
|
+
|
|
37
|
+
const portalSource = `
|
|
38
|
+
export const customDimensions = {
|
|
39
|
+
action: 1,
|
|
40
|
+
serviceName: 24,
|
|
41
|
+
deprecated: 25 // Unused - accidentally added
|
|
42
|
+
} as const
|
|
43
|
+
|
|
44
|
+
export const sessionDimensions = {
|
|
45
|
+
permission: 18,
|
|
46
|
+
organizationName: 26
|
|
47
|
+
} as const
|
|
48
|
+
`
|
|
49
|
+
|
|
50
|
+
const appSource = `
|
|
51
|
+
/* eslint-disable perfectionist/sort-enums */
|
|
52
|
+
export enum SessionCustomDimensions {
|
|
53
|
+
OS = 1,
|
|
54
|
+
hasFriskvard = 2,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export enum CustomDimensions {
|
|
58
|
+
deviceLocale = 18,
|
|
59
|
+
eventTimestamp = 78,
|
|
60
|
+
}
|
|
61
|
+
`
|
|
62
|
+
|
|
63
|
+
test('parses const maps and enums into event and session scopes', () => {
|
|
64
|
+
const booking = parseDimensionSource(bookingSource, 'src/analytics/dimensions.ts')
|
|
65
|
+
assert.deepEqual(
|
|
66
|
+
booking.map((item) => [item.scope, item.slot, item.key]),
|
|
67
|
+
[
|
|
68
|
+
['event', 1, 'clientNumber'],
|
|
69
|
+
['event', 7, 'rowsBooked'],
|
|
70
|
+
['event', 13, 'rowsFailed'],
|
|
71
|
+
],
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
const portal = parseDimensionSource(portalSource, 'src/analytics/dimensions.ts')
|
|
75
|
+
assert.equal(portal.find((item) => item.key === 'deprecated')?.slot, 25)
|
|
76
|
+
assert.equal(portal.find((item) => item.key === 'permission')?.scope, 'session')
|
|
77
|
+
assert.equal(portal.find((item) => item.key === 'serviceName')?.scope, 'event')
|
|
78
|
+
|
|
79
|
+
const app = parseDimensionSource(appSource, 'src/util/piwikDimensions.ts')
|
|
80
|
+
assert.equal(app.find((item) => item.key === 'OS')?.scope, 'session')
|
|
81
|
+
assert.equal(app.find((item) => item.key === 'deviceLocale')?.scope, 'event')
|
|
82
|
+
assert.equal(app.find((item) => item.key === 'eventTimestamp')?.slot, 78)
|
|
83
|
+
})
|
|
84
|
+
|
|
85
|
+
test('rejects a duplicated slot in one scope', () => {
|
|
86
|
+
assert.throws(
|
|
87
|
+
() => parseDimensionSource('export const customDimensions = { a: 1, b: 1 } as const'),
|
|
88
|
+
/Duplicate event slot 1/,
|
|
89
|
+
)
|
|
90
|
+
})
|
|
91
|
+
|
|
92
|
+
test('matches the app id to tracking_id even when the Piwik slot differs', () => {
|
|
93
|
+
const locals = parseDimensionSource(`
|
|
94
|
+
export const customDimensions = { active: 46 } as const
|
|
95
|
+
`)
|
|
96
|
+
const remotes = [
|
|
97
|
+
{
|
|
98
|
+
id: '1',
|
|
99
|
+
name: 'active',
|
|
100
|
+
slot: 29,
|
|
101
|
+
trackingId: 46,
|
|
102
|
+
scope: 'event',
|
|
103
|
+
active: true,
|
|
104
|
+
caseSensitive: false,
|
|
105
|
+
description: 'Active',
|
|
106
|
+
},
|
|
107
|
+
]
|
|
108
|
+
const diff = diffDimensions(locals, remotes)
|
|
109
|
+
assert.equal(diff.inSync.length, 1)
|
|
110
|
+
assert.equal(diff.missing.length, 0)
|
|
111
|
+
assert.equal(diff.inSync[0].remote.slot, 29)
|
|
112
|
+
const report = formatDimensionReport(diff, planCreates(diff.missing, remotes))
|
|
113
|
+
assert.match(report, /│ 29\s+│ 46\s+│ active\s+│ active\s+│ Active\s+│ in sync\s+│ Active\s+│/)
|
|
114
|
+
assert.equal(planCreates(diff.missing, remotes).creates.length, 0)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
test('diff matches on tracking_id and plans creates in order', () => {
|
|
118
|
+
const locals = parseDimensionSource(portalSource)
|
|
119
|
+
const remotes = [
|
|
120
|
+
{ id: '1', name: 'action', slot: 1, trackingId: 1, scope: 'event', active: true, caseSensitive: false, description: '' },
|
|
121
|
+
{ id: '2', name: 'Old name', slot: 8, trackingId: 18, scope: 'session', active: true, caseSensitive: false, description: '' },
|
|
122
|
+
{ id: '3', name: 'extra', slot: 2, trackingId: 2, scope: 'event', active: false, caseSensitive: false, description: '' },
|
|
123
|
+
]
|
|
124
|
+
const diff = diffDimensions(locals, remotes)
|
|
125
|
+
assert.equal(diff.inSync.length, 1)
|
|
126
|
+
assert.equal(diff.nameMismatch[0].local.key, 'permission')
|
|
127
|
+
assert.equal(diff.remoteOnly[0].name, 'extra')
|
|
128
|
+
assert.equal(diff.trackingMismatch[0].remote.trackingId, 18)
|
|
129
|
+
|
|
130
|
+
const plan = planCreates(diff.missing, remotes)
|
|
131
|
+
assert.deepEqual(
|
|
132
|
+
plan.creates.map((item) => `${item.scope}:${item.slot}`),
|
|
133
|
+
[],
|
|
134
|
+
)
|
|
135
|
+
assert.match(plan.blocked[0].reason, /Next tracking_id is 3/)
|
|
136
|
+
const report = formatDimensionReport(diff, plan)
|
|
137
|
+
assert.match(report, /different name/)
|
|
138
|
+
assert.match(report, /cannot add/)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
test('status stays in one column when a name is long', () => {
|
|
142
|
+
const longName = 'bookingConfirmationStep'.repeat(2)
|
|
143
|
+
const locals = parseDimensionSource(`
|
|
144
|
+
export const customDimensions = { ${longName}: 1, short: 2 } as const
|
|
145
|
+
`)
|
|
146
|
+
const remotes = [longName, 'short'].map((name, index) => ({
|
|
147
|
+
id: String(index + 1),
|
|
148
|
+
name,
|
|
149
|
+
slot: index + 1,
|
|
150
|
+
trackingId: index + 1,
|
|
151
|
+
scope: 'event',
|
|
152
|
+
active: true,
|
|
153
|
+
caseSensitive: false,
|
|
154
|
+
description: '',
|
|
155
|
+
}))
|
|
156
|
+
const report = formatDimensionReport(diffDimensions(locals, remotes), { creates: [], blocked: [] })
|
|
157
|
+
const statusAt = report
|
|
158
|
+
.split('\n')
|
|
159
|
+
.filter((line) => line.startsWith('│') && line.includes('in sync'))
|
|
160
|
+
.map((line) => line.indexOf('in sync'))
|
|
161
|
+
assert.equal(statusAt.length, 2)
|
|
162
|
+
assert.equal(statusAt[0], statusAt[1])
|
|
163
|
+
assert.ok(statusAt[0] > longName.length)
|
|
164
|
+
})
|
|
165
|
+
|
|
166
|
+
test('report rows are ordered by scope and numeric slot', () => {
|
|
167
|
+
const locals = parseDimensionSource(`
|
|
168
|
+
export const customDimensions = { a: 1, b: 2, c: 10, d: 13 } as const
|
|
169
|
+
export const sessionDimensions = { permission: 2 } as const
|
|
170
|
+
`)
|
|
171
|
+
const remotes = [1, 2, 10].map((slot) => ({
|
|
172
|
+
id: String(slot),
|
|
173
|
+
name: locals.find((item) => item.scope === 'event' && item.slot === slot)?.key ?? '',
|
|
174
|
+
slot,
|
|
175
|
+
trackingId: slot,
|
|
176
|
+
scope: 'event',
|
|
177
|
+
active: true,
|
|
178
|
+
caseSensitive: false,
|
|
179
|
+
description: '',
|
|
180
|
+
}))
|
|
181
|
+
const diff = diffDimensions(locals, remotes)
|
|
182
|
+
const plan = planCreates(diff.missing, remotes)
|
|
183
|
+
const report = formatDimensionReport(diff, plan)
|
|
184
|
+
const eventAt = report.indexOf('\nevent\n')
|
|
185
|
+
const sessionAt = report.indexOf('\nsession\n')
|
|
186
|
+
assert.ok(eventAt >= 0 && sessionAt > eventAt)
|
|
187
|
+
const ids = report
|
|
188
|
+
.split('\n')
|
|
189
|
+
.filter((line) => line.startsWith('│') && !line.includes('tracking_id'))
|
|
190
|
+
.map((line) => line.split('│')[2].trim())
|
|
191
|
+
assert.deepEqual(ids, ['1', '2', '10', '13', '2'])
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
test('creates the next free slots and blocks a later gap', () => {
|
|
195
|
+
const locals = parseDimensionSource(`
|
|
196
|
+
export const customDimensions = { a: 1, b: 2, c: 4 } as const
|
|
197
|
+
`)
|
|
198
|
+
const plan = planCreates(locals, [])
|
|
199
|
+
assert.deepEqual(
|
|
200
|
+
plan.creates.map((item) => item.slot),
|
|
201
|
+
[1, 2],
|
|
202
|
+
)
|
|
203
|
+
assert.equal(plan.blocked.length, 1)
|
|
204
|
+
assert.match(plan.blocked[0].reason, /Next tracking_id is 3/)
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
test('product slots can be chosen when they are free', () => {
|
|
208
|
+
const locals = parseDimensionSource(`
|
|
209
|
+
export const productDimensions = { color: 2, size: 4 } as const
|
|
210
|
+
`)
|
|
211
|
+
const plan = planCreates(locals, [
|
|
212
|
+
{ id: 'p', name: 'taken', slot: 2, trackingId: 2, scope: 'product', active: true, caseSensitive: false, description: '' },
|
|
213
|
+
])
|
|
214
|
+
assert.deepEqual(
|
|
215
|
+
plan.creates.map((item) => item.key),
|
|
216
|
+
['size'],
|
|
217
|
+
)
|
|
218
|
+
assert.match(plan.blocked[0].reason, /already used/)
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
test('event and session share one tracking_id sequence', () => {
|
|
222
|
+
const locals = parseDimensionSource(`
|
|
223
|
+
export const sessionDimensions = { permission: 1 } as const
|
|
224
|
+
export const customDimensions = { action: 2 } as const
|
|
225
|
+
`)
|
|
226
|
+
const plan = planCreates(locals, [])
|
|
227
|
+
assert.deepEqual(
|
|
228
|
+
plan.creates.map((item) => `${item.scope}:${item.slot}`),
|
|
229
|
+
['session:1', 'event:2'],
|
|
230
|
+
)
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
test('discovers account and website id from env modules', () => {
|
|
234
|
+
const sites = discoverPiwikSites(`
|
|
235
|
+
const production: EnvironmentConstants = {
|
|
236
|
+
environment: 'production',
|
|
237
|
+
piwikUrl: 'https://terveystalo.piwik.pro',
|
|
238
|
+
piwikId: '475754f6-f457-4203-9d8f-c58b510fadba',
|
|
239
|
+
}
|
|
240
|
+
const developmentEnvironment: EnvironmentConstants = {
|
|
241
|
+
environment: 'development',
|
|
242
|
+
piwikProBaseUrl: 'https://terveystalo.piwik.pro',
|
|
243
|
+
piwikProId: '6931c50c-b8ea-4960-8153-6916586ecd5b',
|
|
244
|
+
}
|
|
245
|
+
`)
|
|
246
|
+
assert.equal(accountFromPiwikUrl(sites[0].url), 'terveystalo')
|
|
247
|
+
assert.equal(sites[0].websiteId, '475754f6-f457-4203-9d8f-c58b510fadba')
|
|
248
|
+
assert.equal(sites[1].environment, 'development')
|
|
249
|
+
assert.equal(sites[1].websiteId, '6931c50c-b8ea-4960-8153-6916586ecd5b')
|
|
250
|
+
})
|
|
251
|
+
|
|
252
|
+
test('environments that share a piwik id collapse to one choice', () => {
|
|
253
|
+
const unique = uniquePiwikEnvironments([
|
|
254
|
+
{ environment: 'localhost', account: 'terveystalo', websiteId: '049864b3-ba27-42ca-b89f-ad2b130aa64b' },
|
|
255
|
+
{ environment: 'development', account: 'terveystalo', websiteId: '049864b3-ba27-42ca-b89f-ad2b130aa64b' },
|
|
256
|
+
{ environment: 'preprod', account: 'terveystalo', websiteId: '049864b3-ba27-42ca-b89f-ad2b130aa64b' },
|
|
257
|
+
{ environment: 'production', account: 'terveystalo', websiteId: '475754f6-f457-4203-9d8f-c58b510fadba' },
|
|
258
|
+
])
|
|
259
|
+
assert.deepEqual(
|
|
260
|
+
unique.map((site) => site.environment),
|
|
261
|
+
['preprod', 'production'],
|
|
262
|
+
)
|
|
263
|
+
assert.equal(defaultPiwikEnvironment(unique), 'preprod')
|
|
264
|
+
assert.equal(defaultPiwikEnvironment([{ environment: 'production' }]), 'production')
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
test('normalizes a JSON:API custom dimension', () => {
|
|
268
|
+
const remote = normalizeDimension(
|
|
269
|
+
{
|
|
270
|
+
id: 'abc',
|
|
271
|
+
attributes: {
|
|
272
|
+
name: 'Rows booked',
|
|
273
|
+
slot: 7,
|
|
274
|
+
tracking_id: 7,
|
|
275
|
+
scope: 'event',
|
|
276
|
+
active: true,
|
|
277
|
+
case_sensitive: false,
|
|
278
|
+
website_id: 'site',
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
'event',
|
|
282
|
+
)
|
|
283
|
+
assert.equal(remote.slot, 7)
|
|
284
|
+
assert.equal(remote.scope, 'event')
|
|
285
|
+
assert.equal(remote.websiteId, 'site')
|
|
286
|
+
})
|
|
287
|
+
|
|
288
|
+
test('environment pair defaults to a non-production source and production target', () => {
|
|
289
|
+
assert.deepEqual(
|
|
290
|
+
defaultEnvironmentPair([{ environment: 'preprod' }, { environment: 'production' }]),
|
|
291
|
+
{ from: 'preprod', to: 'production' },
|
|
292
|
+
)
|
|
293
|
+
assert.deepEqual(
|
|
294
|
+
defaultEnvironmentPair([{ environment: 'staging' }, { environment: 'production' }]),
|
|
295
|
+
{ from: 'staging', to: 'production' },
|
|
296
|
+
)
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* @param {Record<string, unknown>} overrides
|
|
301
|
+
*/
|
|
302
|
+
function remoteDimension(overrides) {
|
|
303
|
+
return {
|
|
304
|
+
id: '1',
|
|
305
|
+
name: 'active',
|
|
306
|
+
slot: 1,
|
|
307
|
+
trackingId: 1,
|
|
308
|
+
scope: 'event',
|
|
309
|
+
active: true,
|
|
310
|
+
caseSensitive: false,
|
|
311
|
+
description: '',
|
|
312
|
+
...overrides,
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
test('compares environments on tracking_id and keeps in-sync rows', () => {
|
|
317
|
+
const source = [
|
|
318
|
+
remoteDimension({ id: 'a', name: 'active', slot: 29, trackingId: 46, description: 'Active' }),
|
|
319
|
+
remoteDimension({ id: 'b', name: 'step', slot: 30, trackingId: 47 }),
|
|
320
|
+
]
|
|
321
|
+
const target = [
|
|
322
|
+
remoteDimension({ id: 'c', name: 'active', slot: 12, trackingId: 46, description: 'Active' }),
|
|
323
|
+
]
|
|
324
|
+
const diff = diffEnvironments(source, target)
|
|
325
|
+
assert.equal(diff.inSync.length, 1)
|
|
326
|
+
assert.equal(diff.inSync[0].target.slot, 12)
|
|
327
|
+
assert.equal(diff.missing[0].trackingId, 47)
|
|
328
|
+
const plan = planEnvironmentMigration(diff, target)
|
|
329
|
+
assert.equal(plan.creates.length, 0)
|
|
330
|
+
assert.match(plan.blocked[0].reason, /Next tracking_id is 1/)
|
|
331
|
+
const report = formatEnvironmentReport(diff, plan, 'preprod', 'production')
|
|
332
|
+
assert.match(report, /│ 29\s+│ 46\s+│ active\s+│ 12\s+│ 46\s+│ active\s+│ Active\s+│ in sync\s+│ Active\s+│/)
|
|
333
|
+
assert.match(report, /tracking_id preprod/)
|
|
334
|
+
assert.match(report, /tracking_id production/)
|
|
335
|
+
assert.match(report, /in sync/)
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
test('plans environment migration in tracking_id order and blocks a gap', () => {
|
|
339
|
+
const source = [
|
|
340
|
+
remoteDimension({ name: 'one', trackingId: 1, slot: 1 }),
|
|
341
|
+
remoteDimension({ id: '2', name: 'two', trackingId: 2, slot: 2, description: 'Second' }),
|
|
342
|
+
remoteDimension({ id: '4', name: 'four', trackingId: 4, slot: 4 }),
|
|
343
|
+
]
|
|
344
|
+
const target = [
|
|
345
|
+
remoteDimension({ name: 'old', trackingId: 1, slot: 8, description: 'Old' }),
|
|
346
|
+
remoteDimension({ id: 'x', name: 'extra', trackingId: 9, slot: 9 }),
|
|
347
|
+
]
|
|
348
|
+
const diff = diffEnvironments(source, target)
|
|
349
|
+
assert.deepEqual(diff.changed[0].fields, ['name', 'description'])
|
|
350
|
+
assert.equal(diff.targetOnly[0].name, 'extra')
|
|
351
|
+
const plan = planEnvironmentMigration(diff, target)
|
|
352
|
+
assert.deepEqual(
|
|
353
|
+
plan.creates.map((item) => item.slot),
|
|
354
|
+
[2],
|
|
355
|
+
)
|
|
356
|
+
assert.match(plan.blocked[0].reason, /Next tracking_id is 3/)
|
|
357
|
+
const report = formatEnvironmentReport(diff, plan, 'staging', 'production')
|
|
358
|
+
assert.match(report, /different name, different description/)
|
|
359
|
+
assert.match(report, /cannot add/)
|
|
360
|
+
assert.match(report, /only in production/)
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
test('pull plans tracking id updates from Piwik into the local map', () => {
|
|
364
|
+
const locals = parseDimensionSource(`
|
|
365
|
+
export const customDimensions = { active: 29, step: 30 } as const
|
|
366
|
+
`)
|
|
367
|
+
const remotes = [
|
|
368
|
+
remoteDimension({ name: 'active', slot: 12, trackingId: 46 }),
|
|
369
|
+
remoteDimension({ id: '2', name: 'step', slot: 13, trackingId: 30 }),
|
|
370
|
+
remoteDimension({ id: '3', name: 'extra', slot: 14, trackingId: 50 }),
|
|
371
|
+
]
|
|
372
|
+
const plan = planLocalPull(locals, remotes)
|
|
373
|
+
assert.equal(plan.updates.length, 1)
|
|
374
|
+
assert.equal(plan.updates[0].local.key, 'active')
|
|
375
|
+
assert.equal(plan.updates[0].next, 46)
|
|
376
|
+
assert.equal(plan.inSync.length, 1)
|
|
377
|
+
assert.equal(plan.remoteOnly[0].name, 'extra')
|
|
378
|
+
const source = 'export const customDimensions = { active: 29, step: 30 } as const\n'
|
|
379
|
+
assert.equal(
|
|
380
|
+
rewriteDimensionNumbers(source, [{ key: 'active', from: 29, to: 46 }]),
|
|
381
|
+
'export const customDimensions = { active: 46, step: 30 } as const\n',
|
|
382
|
+
)
|
|
383
|
+
})
|
|
384
|
+
|
|
385
|
+
test('parses the real consumer dimension files when they are checked out beside fg-devkit', () => {
|
|
386
|
+
const files = [
|
|
387
|
+
['../../bokningsverktyget-ui/src/analytics/dimensions.ts', 'event', 'rowsBooked', 7],
|
|
388
|
+
['../../webbportalen-ui/src/analytics/dimensions.ts', 'session', 'organizationName', 26],
|
|
389
|
+
['../../feelgood-app/src/util/piwikDimensions.ts', 'event', 'eventTimestamp', 78],
|
|
390
|
+
]
|
|
391
|
+
for (const [relative, scope, key, slot] of files) {
|
|
392
|
+
const file = path.join(scriptDir, relative)
|
|
393
|
+
if (!fs.existsSync(file)) continue
|
|
394
|
+
const dimensions = parseDimensionSource(fs.readFileSync(file, 'utf8'), relative)
|
|
395
|
+
const found = dimensions.find((item) => item.key === key)
|
|
396
|
+
assert.ok(found, `${key} missing in ${relative}`)
|
|
397
|
+
assert.equal(found.scope, scope)
|
|
398
|
+
assert.equal(found.slot, slot)
|
|
399
|
+
}
|
|
400
|
+
})
|