tm1npm 2.0.0 → 2.1.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/lib/objects/Axis.d.ts +1 -0
- package/lib/objects/Axis.d.ts.map +1 -1
- package/lib/objects/Axis.js +3 -0
- package/lib/objects/Chore.d.ts +2 -2
- package/lib/objects/Chore.d.ts.map +1 -1
- package/lib/objects/Chore.js +7 -13
- package/lib/objects/Cube.d.ts.map +1 -1
- package/lib/objects/Cube.js +2 -1
- package/lib/objects/Hierarchy.js +10 -10
- package/lib/objects/MDXView.d.ts +2 -0
- package/lib/objects/MDXView.d.ts.map +1 -1
- package/lib/objects/MDXView.js +30 -9
- package/lib/objects/NativeView.d.ts +5 -5
- package/lib/objects/NativeView.d.ts.map +1 -1
- package/lib/objects/NativeView.js +17 -34
- package/lib/objects/Process.d.ts +8 -3
- package/lib/objects/Process.d.ts.map +1 -1
- package/lib/objects/Process.js +143 -33
- package/lib/objects/Subset.d.ts.map +1 -1
- package/lib/objects/Subset.js +10 -3
- package/lib/objects/User.d.ts +5 -5
- package/lib/objects/User.d.ts.map +1 -1
- package/lib/objects/User.js +14 -23
- package/lib/tests/debuggerService.test.js +3 -1
- package/lib/tests/objectModelParity.test.js +362 -11
- package/lib/tests/objects.improved.test.js +28 -5
- package/lib/tests/user.issue61.test.d.ts +2 -0
- package/lib/tests/user.issue61.test.d.ts.map +1 -0
- package/lib/tests/user.issue61.test.js +180 -0
- package/lib/utils/Utils.d.ts +6 -1
- package/lib/utils/Utils.d.ts.map +1 -1
- package/lib/utils/Utils.js +56 -7
- package/package.json +1 -1
- package/src/objects/Axis.ts +4 -0
- package/src/objects/Chore.ts +7 -14
- package/src/objects/Cube.ts +2 -1
- package/src/objects/Hierarchy.ts +11 -11
- package/src/objects/MDXView.ts +29 -9
- package/src/objects/NativeView.ts +26 -42
- package/src/objects/Process.ts +182 -66
- package/src/objects/Subset.ts +17 -3
- package/src/objects/User.ts +17 -23
- package/src/tests/debuggerService.test.ts +3 -1
- package/src/tests/objectModelParity.test.ts +456 -11
- package/src/tests/objects.improved.test.ts +41 -9
- package/src/tests/user.issue61.test.ts +206 -0
- package/src/utils/Utils.ts +60 -7
|
@@ -11,6 +11,10 @@ const MDXView_1 = require("../objects/MDXView");
|
|
|
11
11
|
const Hierarchy_1 = require("../objects/Hierarchy");
|
|
12
12
|
const Element_1 = require("../objects/Element");
|
|
13
13
|
const ElementAttribute_1 = require("../objects/ElementAttribute");
|
|
14
|
+
const Chore_1 = require("../objects/Chore");
|
|
15
|
+
const ChoreFrequency_1 = require("../objects/ChoreFrequency");
|
|
16
|
+
const ChoreStartTime_1 = require("../objects/ChoreStartTime");
|
|
17
|
+
const Utils_1 = require("../utils/Utils");
|
|
14
18
|
// ---------------------------------------------------------------------------
|
|
15
19
|
// Bug 1 & 2 (Axis.ts): ViewAxisSelection.fromDict and ViewTitleSelection.fromDict
|
|
16
20
|
// ---------------------------------------------------------------------------
|
|
@@ -257,6 +261,116 @@ describe('NativeView.fromDict', () => {
|
|
|
257
261
|
});
|
|
258
262
|
});
|
|
259
263
|
// ---------------------------------------------------------------------------
|
|
264
|
+
// Issue #75: NativeView fromDict, remove methods, substituteTitle parity
|
|
265
|
+
// ---------------------------------------------------------------------------
|
|
266
|
+
describe('NativeView.fromDict — title validation (issue #75)', () => {
|
|
267
|
+
test("throws when title dict lacks both 'Selected' and 'Selected@odata.bind'", () => {
|
|
268
|
+
const dict = {
|
|
269
|
+
Name: 'V',
|
|
270
|
+
SuppressEmptyColumns: false,
|
|
271
|
+
SuppressEmptyRows: false,
|
|
272
|
+
FormatString: '0.#',
|
|
273
|
+
Titles: [
|
|
274
|
+
{
|
|
275
|
+
'Subset@odata.bind': "Dimensions('Region')/Hierarchies('Region')/Subsets('All')"
|
|
276
|
+
}
|
|
277
|
+
],
|
|
278
|
+
Columns: [],
|
|
279
|
+
Rows: []
|
|
280
|
+
};
|
|
281
|
+
expect(() => NativeView_1.NativeView.fromDict(dict, 'Cube')).toThrow("View Title dict must contain 'Selected' or 'Selected@odata.bind' as key");
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
describe('NativeView.fromDict — cubeName from @odata.context (issue #75)', () => {
|
|
285
|
+
test('extracts cubeName from @odata.context when not provided', () => {
|
|
286
|
+
const dict = {
|
|
287
|
+
'@odata.context': "../$metadata#Cubes('SalesCube')/Views/$entity",
|
|
288
|
+
Name: 'V',
|
|
289
|
+
SuppressEmptyColumns: false,
|
|
290
|
+
SuppressEmptyRows: false,
|
|
291
|
+
FormatString: '0.#',
|
|
292
|
+
Titles: [],
|
|
293
|
+
Columns: [],
|
|
294
|
+
Rows: []
|
|
295
|
+
};
|
|
296
|
+
const view = NativeView_1.NativeView.fromDict(dict);
|
|
297
|
+
expect(view.cube).toBe('SalesCube');
|
|
298
|
+
});
|
|
299
|
+
test('prefers explicit cubeName over @odata.context', () => {
|
|
300
|
+
const dict = {
|
|
301
|
+
'@odata.context': "../$metadata#Cubes('WrongCube')/Views/$entity",
|
|
302
|
+
Name: 'V',
|
|
303
|
+
SuppressEmptyColumns: false,
|
|
304
|
+
SuppressEmptyRows: false,
|
|
305
|
+
FormatString: '0.#',
|
|
306
|
+
Titles: [],
|
|
307
|
+
Columns: [],
|
|
308
|
+
Rows: []
|
|
309
|
+
};
|
|
310
|
+
const view = NativeView_1.NativeView.fromDict(dict, 'CorrectCube');
|
|
311
|
+
expect(view.cube).toBe('CorrectCube');
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
describe('NativeView.removeColumn / removeRow / removeTitle — remove ALL matches (issue #75)', () => {
|
|
315
|
+
test('removeColumn removes all matching columns', () => {
|
|
316
|
+
const view = new NativeView_1.NativeView('Cube', 'View');
|
|
317
|
+
view.addColumn(new Axis_1.ViewAxisSelection('Region', new Subset_1.AnonymousSubset('Region', 'Region', undefined, ['North'])));
|
|
318
|
+
view.addColumn(new Axis_1.ViewAxisSelection('Region', new Subset_1.AnonymousSubset('Region', 'Region', undefined, ['South'])));
|
|
319
|
+
view.addColumn(new Axis_1.ViewAxisSelection('Month', new Subset_1.AnonymousSubset('Month', 'Month', undefined, ['Jan'])));
|
|
320
|
+
view.removeColumn('Region');
|
|
321
|
+
expect(view.columns).toHaveLength(1);
|
|
322
|
+
expect(view.columns[0].dimensionName).toBe('Month');
|
|
323
|
+
});
|
|
324
|
+
test('removeRow removes all matching rows', () => {
|
|
325
|
+
const view = new NativeView_1.NativeView('Cube', 'View');
|
|
326
|
+
view.addRow(new Axis_1.ViewAxisSelection('Region', new Subset_1.AnonymousSubset('Region', 'Region', undefined, ['North'])));
|
|
327
|
+
view.addRow(new Axis_1.ViewAxisSelection('Region', new Subset_1.AnonymousSubset('Region', 'Region', undefined, ['South'])));
|
|
328
|
+
view.addRow(new Axis_1.ViewAxisSelection('Month', new Subset_1.AnonymousSubset('Month', 'Month', undefined, ['Jan'])));
|
|
329
|
+
view.removeRow('Region');
|
|
330
|
+
expect(view.rows).toHaveLength(1);
|
|
331
|
+
expect(view.rows[0].dimensionName).toBe('Month');
|
|
332
|
+
});
|
|
333
|
+
test('removeTitle removes all matching titles', () => {
|
|
334
|
+
const view = new NativeView_1.NativeView('Cube', 'View');
|
|
335
|
+
const sub = new Subset_1.AnonymousSubset('Region', 'Region', undefined, ['North']);
|
|
336
|
+
view.addTitle(new Axis_1.ViewTitleSelection('Region', sub, 'North'));
|
|
337
|
+
view.addTitle(new Axis_1.ViewTitleSelection('Region', sub, 'South'));
|
|
338
|
+
view.addTitle(new Axis_1.ViewTitleSelection('Month', new Subset_1.AnonymousSubset('Month', 'Month', undefined, ['Jan']), 'Jan'));
|
|
339
|
+
view.removeTitle('Region');
|
|
340
|
+
expect(view.titles).toHaveLength(1);
|
|
341
|
+
expect(view.titles[0].dimensionName).toBe('Month');
|
|
342
|
+
});
|
|
343
|
+
test('removeColumn is case and space insensitive', () => {
|
|
344
|
+
const view = new NativeView_1.NativeView('Cube', 'View');
|
|
345
|
+
view.addColumn(new Axis_1.ViewAxisSelection('Region', new Subset_1.AnonymousSubset('Region', 'Region', undefined, ['North'])));
|
|
346
|
+
view.removeColumn(' region ');
|
|
347
|
+
expect(view.columns).toHaveLength(0);
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
describe('NativeView.substituteTitle — replaces subset and selected (issue #75)', () => {
|
|
351
|
+
test('replaces subset with AnonymousSubset(dim, dim, [element]) and updates selected', () => {
|
|
352
|
+
const view = new NativeView_1.NativeView('Cube', 'View');
|
|
353
|
+
view.addTitle(new Axis_1.ViewTitleSelection('Region', new Subset_1.AnonymousSubset('Region', 'Region', undefined, ['North', 'South']), 'North'));
|
|
354
|
+
view.substituteTitle('Region', 'South');
|
|
355
|
+
expect(view.titles[0].selected).toBe('South');
|
|
356
|
+
const newSubset = view.titles[0].subset;
|
|
357
|
+
expect(newSubset).toBeInstanceOf(Subset_1.AnonymousSubset);
|
|
358
|
+
expect(newSubset.elements).toEqual(['South']);
|
|
359
|
+
});
|
|
360
|
+
test('new subset uses caller-provided dimension as both dimension and hierarchy', () => {
|
|
361
|
+
const view = new NativeView_1.NativeView('Cube', 'View');
|
|
362
|
+
view.addTitle(new Axis_1.ViewTitleSelection('Region', new Subset_1.AnonymousSubset('Region', 'AltHierarchy', undefined, ['North']), 'North'));
|
|
363
|
+
view.substituteTitle('Region', 'South');
|
|
364
|
+
const newSubset = view.titles[0].subset;
|
|
365
|
+
expect(newSubset.dimensionName).toBe('Region');
|
|
366
|
+
expect(newSubset.hierarchyName).toBe('Region');
|
|
367
|
+
});
|
|
368
|
+
test("throws \"Dimension '...' not found in titles\" when dimension missing", () => {
|
|
369
|
+
const view = new NativeView_1.NativeView('Cube', 'View');
|
|
370
|
+
expect(() => view.substituteTitle('NonExistent', 'Value')).toThrow("Dimension 'NonExistent' not found in titles");
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
// ---------------------------------------------------------------------------
|
|
260
374
|
// Bug 4 & 5 (Hierarchy.ts): Edge key type + traversal methods
|
|
261
375
|
// ---------------------------------------------------------------------------
|
|
262
376
|
describe('Hierarchy edges (Map<string, Map<string, number>>)', () => {
|
|
@@ -447,28 +561,84 @@ describe('MDXView.dynamicProperties', () => {
|
|
|
447
561
|
const view = new MDXView_1.MDXView('Cube', 'View', 'SELECT ...', props);
|
|
448
562
|
expect(view.dynamicProperties).toEqual(props);
|
|
449
563
|
});
|
|
450
|
-
test('fromDict
|
|
564
|
+
test('fromDict treats top-level non-excluded keys as dynamicProperties', () => {
|
|
451
565
|
const dict = {
|
|
566
|
+
Cube: { Name: 'C' },
|
|
452
567
|
Name: 'V',
|
|
453
568
|
MDX: 'SELECT ...',
|
|
454
|
-
|
|
569
|
+
Aliases: ['Default'],
|
|
570
|
+
Meta: { foo: 1 },
|
|
455
571
|
};
|
|
456
|
-
const view = MDXView_1.MDXView.fromDict(dict
|
|
457
|
-
expect(view.dynamicProperties).toEqual({ Aliases: ['Default'] });
|
|
572
|
+
const view = MDXView_1.MDXView.fromDict(dict);
|
|
573
|
+
expect(view.dynamicProperties).toEqual({ Aliases: ['Default'], Meta: { foo: 1 } });
|
|
458
574
|
});
|
|
459
|
-
test('fromDict
|
|
460
|
-
const
|
|
575
|
+
test('fromDict yields empty dynamicProperties when only excluded keys present', () => {
|
|
576
|
+
const dict = { Cube: { Name: 'C' }, Name: 'V', MDX: 'SELECT ...' };
|
|
577
|
+
const view = MDXView_1.MDXView.fromDict(dict);
|
|
461
578
|
expect(view.dynamicProperties).toEqual({});
|
|
462
579
|
});
|
|
463
|
-
test('
|
|
464
|
-
const
|
|
580
|
+
test('fromDict prefers explicit cubeName over dict.Cube.Name', () => {
|
|
581
|
+
const dict = { Cube: { Name: 'FromDict' }, Name: 'V', MDX: 'SELECT ...' };
|
|
582
|
+
const view = MDXView_1.MDXView.fromDict(dict, 'Explicit');
|
|
583
|
+
expect(view.cube).toBe('Explicit');
|
|
584
|
+
});
|
|
585
|
+
test('fromDict falls back to dict.Cube.Name when cubeName is not provided', () => {
|
|
586
|
+
const dict = { Cube: { Name: 'FromDict' }, Name: 'V', MDX: 'SELECT ...' };
|
|
587
|
+
const view = MDXView_1.MDXView.fromDict(dict);
|
|
588
|
+
expect(view.cube).toBe('FromDict');
|
|
589
|
+
});
|
|
590
|
+
test('fromDict throws when no cubeName and no Cube field', () => {
|
|
591
|
+
const dict = { Name: 'V', MDX: 'SELECT ...' };
|
|
592
|
+
expect(() => MDXView_1.MDXView.fromDict(dict)).toThrow();
|
|
593
|
+
});
|
|
594
|
+
test('fromDict preserves a top-level Properties key as dynamicProperties.Properties', () => {
|
|
595
|
+
const dict = {
|
|
596
|
+
Cube: { Name: 'C' },
|
|
597
|
+
Name: 'V',
|
|
598
|
+
MDX: 'SELECT ...',
|
|
599
|
+
Properties: { Aliases: ['Default'] },
|
|
600
|
+
};
|
|
601
|
+
const view = MDXView_1.MDXView.fromDict(dict);
|
|
602
|
+
expect(view.dynamicProperties).toEqual({ Properties: { Aliases: ['Default'] } });
|
|
603
|
+
});
|
|
604
|
+
test.each([
|
|
605
|
+
'@odata.type',
|
|
606
|
+
'@odata.context',
|
|
607
|
+
'@odata.etag',
|
|
608
|
+
'Name',
|
|
609
|
+
'MDX',
|
|
610
|
+
'Cube',
|
|
611
|
+
'Attributes',
|
|
612
|
+
'LocalizedAttributes',
|
|
613
|
+
])('fromDict filters excluded key %s', (excludedKey) => {
|
|
614
|
+
const dict = {
|
|
615
|
+
Cube: { Name: 'C' },
|
|
616
|
+
Name: 'V',
|
|
617
|
+
MDX: 'SELECT ...',
|
|
618
|
+
Foo: 1,
|
|
619
|
+
};
|
|
620
|
+
dict[excludedKey] = 'should-be-stripped';
|
|
621
|
+
const view = MDXView_1.MDXView.fromDict(dict, 'Cube');
|
|
622
|
+
expect(view.dynamicProperties).not.toHaveProperty(excludedKey);
|
|
623
|
+
expect(view.dynamicProperties).toEqual({ Foo: 1 });
|
|
624
|
+
});
|
|
625
|
+
test('body merges dynamicProperties as top-level keys', () => {
|
|
626
|
+
const view = new MDXView_1.MDXView('Cube', 'View', 'SELECT ...', { ContextSets: [], Meta: 1 });
|
|
465
627
|
const body = JSON.parse(view.body);
|
|
466
|
-
expect(body.
|
|
628
|
+
expect(body.ContextSets).toEqual([]);
|
|
629
|
+
expect(body.Meta).toBe(1);
|
|
630
|
+
expect(body.Properties).toBeUndefined();
|
|
467
631
|
});
|
|
468
|
-
test('body
|
|
632
|
+
test('body strips excluded keys from dynamicProperties before serializing', () => {
|
|
633
|
+
const view = new MDXView_1.MDXView('Cube', 'View', 'SELECT ...', { Name: 'IgnoreMe', Foo: 'bar' });
|
|
634
|
+
const body = JSON.parse(view.body);
|
|
635
|
+
expect(body.Name).toBe('View');
|
|
636
|
+
expect(body.Foo).toBe('bar');
|
|
637
|
+
});
|
|
638
|
+
test('body omits dynamicProperties keys when empty', () => {
|
|
469
639
|
const view = new MDXView_1.MDXView('Cube', 'View', 'SELECT ...');
|
|
470
640
|
const body = JSON.parse(view.body);
|
|
471
|
-
expect(body).
|
|
641
|
+
expect(Object.keys(body).sort()).toEqual(['@odata.type', 'MDX', 'Name'].sort());
|
|
472
642
|
});
|
|
473
643
|
test('dynamicProperties round-trips through body and fromDict', () => {
|
|
474
644
|
const props = { Meta: true, Aliases: ['Default'] };
|
|
@@ -508,4 +678,185 @@ describe('ElementAttribute.equals', () => {
|
|
|
508
678
|
expect(a.equals(b)).toBe(false);
|
|
509
679
|
});
|
|
510
680
|
});
|
|
681
|
+
describe('ChoreStartTime.add (Issue #73)', () => {
|
|
682
|
+
const buildStartTime = () => new ChoreStartTime_1.ChoreStartTime(2020, 1, 1, 0, 0, 0);
|
|
683
|
+
test('adds positive days hours minutes seconds to start time', () => {
|
|
684
|
+
const st = buildStartTime();
|
|
685
|
+
st.add(1, 2, 3, 4);
|
|
686
|
+
expect(st.datetime.getFullYear()).toBe(2020);
|
|
687
|
+
expect(st.datetime.getMonth()).toBe(0);
|
|
688
|
+
expect(st.datetime.getDate()).toBe(2);
|
|
689
|
+
expect(st.datetime.getHours()).toBe(2);
|
|
690
|
+
expect(st.datetime.getMinutes()).toBe(3);
|
|
691
|
+
expect(st.datetime.getSeconds()).toBe(4);
|
|
692
|
+
});
|
|
693
|
+
test('default arguments are all zero (no mutation)', () => {
|
|
694
|
+
const st = buildStartTime();
|
|
695
|
+
const originalMs = st.datetime.getTime();
|
|
696
|
+
st.add();
|
|
697
|
+
expect(st.datetime.getTime()).toBe(originalMs);
|
|
698
|
+
});
|
|
699
|
+
test('negative values subtract time', () => {
|
|
700
|
+
const st = new ChoreStartTime_1.ChoreStartTime(2020, 1, 2, 0, 0, 0);
|
|
701
|
+
st.add(-1, 0, 0, 0);
|
|
702
|
+
expect(st.datetime.getFullYear()).toBe(2020);
|
|
703
|
+
expect(st.datetime.getMonth()).toBe(0);
|
|
704
|
+
expect(st.datetime.getDate()).toBe(1);
|
|
705
|
+
});
|
|
706
|
+
test('hours overflow into days (parity with Python timedelta normalization)', () => {
|
|
707
|
+
const st = buildStartTime();
|
|
708
|
+
st.add(0, 25, 0, 0);
|
|
709
|
+
expect(st.datetime.getDate()).toBe(2);
|
|
710
|
+
expect(st.datetime.getHours()).toBe(1);
|
|
711
|
+
});
|
|
712
|
+
test('seconds overflow into minutes', () => {
|
|
713
|
+
const st = buildStartTime();
|
|
714
|
+
st.add(0, 0, 0, 75);
|
|
715
|
+
expect(st.datetime.getMinutes()).toBe(1);
|
|
716
|
+
expect(st.datetime.getSeconds()).toBe(15);
|
|
717
|
+
});
|
|
718
|
+
});
|
|
719
|
+
describe('Chore.reschedule (Issue #73)', () => {
|
|
720
|
+
const buildChore = () => new Chore_1.Chore('TestChore', new ChoreStartTime_1.ChoreStartTime(2020, 1, 1, 0, 0, 0), false, true, Chore_1.Chore.SINGLE_COMMIT, new ChoreFrequency_1.ChoreFrequency(0, 0, 0, 0), []);
|
|
721
|
+
test('reschedule(days, hours, minutes, seconds) mutates start time and leaves frequency unchanged', () => {
|
|
722
|
+
const chore = buildChore();
|
|
723
|
+
const originalFrequency = chore.frequency;
|
|
724
|
+
chore.reschedule(1, 2, 3, 4);
|
|
725
|
+
expect(chore.startTime.datetime.getDate()).toBe(2);
|
|
726
|
+
expect(chore.startTime.datetime.getHours()).toBe(2);
|
|
727
|
+
expect(chore.startTime.datetime.getMinutes()).toBe(3);
|
|
728
|
+
expect(chore.startTime.datetime.getSeconds()).toBe(4);
|
|
729
|
+
expect(chore.frequency).toBe(originalFrequency);
|
|
730
|
+
});
|
|
731
|
+
test('reschedule defaults all components to 0 (no-op)', () => {
|
|
732
|
+
const chore = buildChore();
|
|
733
|
+
const originalMs = chore.startTime.datetime.getTime();
|
|
734
|
+
chore.reschedule();
|
|
735
|
+
expect(chore.startTime.datetime.getTime()).toBe(originalMs);
|
|
736
|
+
});
|
|
737
|
+
test('reschedule with negative values subtracts from start time', () => {
|
|
738
|
+
const chore = new Chore_1.Chore('TestChore', new ChoreStartTime_1.ChoreStartTime(2020, 1, 2, 0, 0, 0), false, true, Chore_1.Chore.SINGLE_COMMIT, new ChoreFrequency_1.ChoreFrequency(0, 0, 0, 0), []);
|
|
739
|
+
chore.reschedule(-1, 0, 0, 0);
|
|
740
|
+
expect(chore.startTime.datetime.getDate()).toBe(1);
|
|
741
|
+
});
|
|
742
|
+
});
|
|
743
|
+
describe('Chore.setFrequency (tm1npm-only helper, Issue #73)', () => {
|
|
744
|
+
const buildChore = () => new Chore_1.Chore('TestChore', new ChoreStartTime_1.ChoreStartTime(2020, 1, 1, 0, 0, 0), false, true, Chore_1.Chore.SINGLE_COMMIT, new ChoreFrequency_1.ChoreFrequency(1, 2, 3, 4), []);
|
|
745
|
+
test('replaces frequency without changing start time when startTime omitted', () => {
|
|
746
|
+
const chore = buildChore();
|
|
747
|
+
const originalStartMs = chore.startTime.datetime.getTime();
|
|
748
|
+
const newFreq = new ChoreFrequency_1.ChoreFrequency(5, 6, 7, 8);
|
|
749
|
+
chore.setFrequency(newFreq);
|
|
750
|
+
expect(chore.frequency).toBe(newFreq);
|
|
751
|
+
expect(chore.startTime.datetime.getTime()).toBe(originalStartMs);
|
|
752
|
+
});
|
|
753
|
+
test('replaces both frequency and start time when startTime provided', () => {
|
|
754
|
+
const chore = buildChore();
|
|
755
|
+
const newFreq = new ChoreFrequency_1.ChoreFrequency(5, 6, 7, 8);
|
|
756
|
+
const newStart = new ChoreStartTime_1.ChoreStartTime(2025, 6, 15, 12, 0, 0);
|
|
757
|
+
chore.setFrequency(newFreq, newStart);
|
|
758
|
+
expect(chore.frequency).toBe(newFreq);
|
|
759
|
+
expect(chore.startTime).toBe(newStart);
|
|
760
|
+
});
|
|
761
|
+
});
|
|
762
|
+
// ---------------------------------------------------------------------------
|
|
763
|
+
// Issue #70 (Subset.ts): AnonymousSubset.fromDict URL parsing
|
|
764
|
+
//
|
|
765
|
+
// These tests lock in tm1py's literal behavior — INCLUDING the latent bug
|
|
766
|
+
// where read_object_name_from_url always returns match.group(1), causing both
|
|
767
|
+
// segments to resolve to the dimension name. Do NOT "fix" these tests to
|
|
768
|
+
// reflect more "correct" behavior — see CLAUDE.md "Strict tm1py Parity".
|
|
769
|
+
// ---------------------------------------------------------------------------
|
|
770
|
+
describe('AnonymousSubset.fromDict — Hierarchy@odata.bind parsing (#70)', () => {
|
|
771
|
+
test('parses dimension and hierarchy when names match', () => {
|
|
772
|
+
const sub = Subset_1.AnonymousSubset.fromDict({
|
|
773
|
+
'Hierarchy@odata.bind': "Dimensions('Region')/Hierarchies('Region')",
|
|
774
|
+
Elements: [{ Name: 'North' }]
|
|
775
|
+
});
|
|
776
|
+
expect(sub.dimensionName).toBe('Region');
|
|
777
|
+
expect(sub.hierarchyName).toBe('Region');
|
|
778
|
+
});
|
|
779
|
+
test('non-default hierarchy resolves to dimension name (tm1py bug parity)', () => {
|
|
780
|
+
// tm1py's read_object_name_from_url returns match.group(1) twice, both
|
|
781
|
+
// capturing the dimension. Hierarchy name in the URL is silently lost.
|
|
782
|
+
const sub = Subset_1.AnonymousSubset.fromDict({
|
|
783
|
+
'Hierarchy@odata.bind': "Dimensions('Region')/Hierarchies('Region_Alt')",
|
|
784
|
+
Expression: '{[Region].[Region_Alt].Members}'
|
|
785
|
+
});
|
|
786
|
+
expect(sub.dimensionName).toBe('Region');
|
|
787
|
+
expect(sub.hierarchyName).toBe('Region');
|
|
788
|
+
});
|
|
789
|
+
test('parses Hierarchy entity form correctly', () => {
|
|
790
|
+
const sub = Subset_1.AnonymousSubset.fromDict({
|
|
791
|
+
Hierarchy: { Name: 'Region_Alt', Dimension: { Name: 'Region' } },
|
|
792
|
+
Elements: [{ Name: 'North' }]
|
|
793
|
+
});
|
|
794
|
+
expect(sub.dimensionName).toBe('Region');
|
|
795
|
+
expect(sub.hierarchyName).toBe('Region_Alt');
|
|
796
|
+
});
|
|
797
|
+
test('throws for URL missing Hierarchies segment', () => {
|
|
798
|
+
expect(() => Subset_1.AnonymousSubset.fromDict({
|
|
799
|
+
'Hierarchy@odata.bind': "Dimensions('Region')"
|
|
800
|
+
})).toThrow(/Unexpected value for 'Hierarchy@odata.bind'/);
|
|
801
|
+
});
|
|
802
|
+
test('throws for non-Dimensions/Hierarchies URL shape (tm1py shape validation)', () => {
|
|
803
|
+
// tm1py's regex anchors on Dimensions(...)/Hierarchies(...) — other shapes fail.
|
|
804
|
+
expect(() => Subset_1.AnonymousSubset.fromDict({
|
|
805
|
+
'Hierarchy@odata.bind': "Foo('A')/Bar('B')"
|
|
806
|
+
})).toThrow(/Unexpected value for 'Hierarchy@odata.bind'/);
|
|
807
|
+
});
|
|
808
|
+
test('throws when neither Hierarchy nor Hierarchy@odata.bind present', () => {
|
|
809
|
+
expect(() => Subset_1.AnonymousSubset.fromDict({})).toThrow(/must contain 'Hierarchy'/);
|
|
810
|
+
});
|
|
811
|
+
test('round-trip: alt hierarchy in URL is dropped via tm1py-equivalent parsing', () => {
|
|
812
|
+
// Input has Hierarchies('Region_Alt') but tm1py's parsing collapses both
|
|
813
|
+
// to the dimension name, so bodyAsDict reflects Hierarchies('Region').
|
|
814
|
+
const sub = Subset_1.AnonymousSubset.fromDict({
|
|
815
|
+
'Hierarchy@odata.bind': "Dimensions('Region')/Hierarchies('Region_Alt')",
|
|
816
|
+
Elements: [{ Name: 'North' }]
|
|
817
|
+
});
|
|
818
|
+
const body = sub.bodyAsDict;
|
|
819
|
+
expect(body['Hierarchy@odata.bind']).toBe("Dimensions('Region')/Hierarchies('Region')");
|
|
820
|
+
});
|
|
821
|
+
});
|
|
822
|
+
describe('ViewAxisSelection.fromDict — anonymous subset with non-default hierarchy (#70)', () => {
|
|
823
|
+
test('hierarchy in URL collapses to dimension name (tm1py bug parity)', () => {
|
|
824
|
+
const dict = {
|
|
825
|
+
Subset: {
|
|
826
|
+
'Hierarchy@odata.bind': "Dimensions('Region')/Hierarchies('Region_Alt')",
|
|
827
|
+
Elements: [{ Name: 'North' }]
|
|
828
|
+
}
|
|
829
|
+
};
|
|
830
|
+
const sel = Axis_1.ViewAxisSelection.fromDict(dict);
|
|
831
|
+
expect(sel.dimensionName).toBe('Region');
|
|
832
|
+
const anon = sel.subset;
|
|
833
|
+
expect(anon.dimensionName).toBe('Region');
|
|
834
|
+
expect(anon.hierarchyName).toBe('Region');
|
|
835
|
+
});
|
|
836
|
+
});
|
|
837
|
+
describe('readObjectNameFromUrl — tm1py parity (#70)', () => {
|
|
838
|
+
test('with pattern: URL-decodes percent-encoded names (mirrors urllib.parse.unquote)', () => {
|
|
839
|
+
const result = (0, Utils_1.readObjectNameFromUrl)("Dimensions('My%20Dim')/Hierarchies('H')", /^Dimensions\('(.+?)'\)/);
|
|
840
|
+
expect(result).toBe('My Dim');
|
|
841
|
+
});
|
|
842
|
+
test('with pattern: passes through malformed %XX (urllib.unquote permissive parity)', () => {
|
|
843
|
+
// Python: urllib.parse.unquote('Bad%ZZName') === 'Bad%ZZName'
|
|
844
|
+
// JS decodeURIComponent throws on malformed %XX — we catch and pass through.
|
|
845
|
+
const result = (0, Utils_1.readObjectNameFromUrl)("Dimensions('Bad%ZZName')/Hierarchies('H')", /^Dimensions\('(.+?)'\)/);
|
|
846
|
+
expect(result).toBe('Bad%ZZName');
|
|
847
|
+
});
|
|
848
|
+
test('with pattern: returns null on no match (mirrors re.match returning None)', () => {
|
|
849
|
+
const result = (0, Utils_1.readObjectNameFromUrl)("Foo('A')/Bar('B')", /^Dimensions\('(.+?)'\)/);
|
|
850
|
+
expect(result).toBeNull();
|
|
851
|
+
});
|
|
852
|
+
test('without pattern: preserves existing string return type (no API break)', () => {
|
|
853
|
+
// Type-level guarantee via overload — runtime check that result is still a string.
|
|
854
|
+
const result = (0, Utils_1.readObjectNameFromUrl)("Dimensions('Region')");
|
|
855
|
+
expect(result).toBe('Region');
|
|
856
|
+
});
|
|
857
|
+
test('without pattern: returns empty string on no match (backward compat)', () => {
|
|
858
|
+
const result = (0, Utils_1.readObjectNameFromUrl)('');
|
|
859
|
+
expect(result).toBe('');
|
|
860
|
+
});
|
|
861
|
+
});
|
|
511
862
|
//# sourceMappingURL=objectModelParity.test.js.map
|
|
@@ -76,7 +76,7 @@ describe('Object Model - Improved Coverage', () => {
|
|
|
76
76
|
const elements = [element1, element2];
|
|
77
77
|
const hierarchy = new Hierarchy_1.Hierarchy('TestHierarchy', 'TestDimension', elements);
|
|
78
78
|
expect(hierarchy.elements).toHaveLength(2);
|
|
79
|
-
expect(hierarchy.elementNames).toEqual(['
|
|
79
|
+
expect(hierarchy.elementNames).toEqual(['Element1', 'Element2']);
|
|
80
80
|
});
|
|
81
81
|
test('should get hierarchy body', () => {
|
|
82
82
|
const hierarchy = new Hierarchy_1.Hierarchy('TestHierarchy', 'TestDimension', []);
|
|
@@ -198,6 +198,18 @@ describe('Object Model - Improved Coverage', () => {
|
|
|
198
198
|
expect(process.hasSecurityAccess).toBe(false);
|
|
199
199
|
// Process automatically adds generated statements to procedures
|
|
200
200
|
});
|
|
201
|
+
test('replaces ALL € with \\f in variablesUiData entries (parity with Python str.replace)', () => {
|
|
202
|
+
// tm1py: each entry passes through entry.replace("€", "\f"), which is
|
|
203
|
+
// replace-all in Python. JS String.replace(stringPattern, ...) replaces
|
|
204
|
+
// only the first occurrence — must use a global regex for parity.
|
|
205
|
+
const entry = 'A€B€C€D';
|
|
206
|
+
const process = new Process_1.Process('TestProcess', false, '', [], [], [entry, 'no-euro-here', '€€€']);
|
|
207
|
+
expect(process.variablesUiData).toEqual([
|
|
208
|
+
'A\fB\fC\fD',
|
|
209
|
+
'no-euro-here',
|
|
210
|
+
'\f\f\f',
|
|
211
|
+
]);
|
|
212
|
+
});
|
|
201
213
|
});
|
|
202
214
|
describe('Cube Object', () => {
|
|
203
215
|
test('should create basic cube', () => {
|
|
@@ -212,7 +224,18 @@ describe('Object Model - Improved Coverage', () => {
|
|
|
212
224
|
const cube = new Cube_1.Cube('TestCube', dimensions);
|
|
213
225
|
const body = JSON.parse(cube.body);
|
|
214
226
|
expect(body.Name).toBe('TestCube');
|
|
215
|
-
expect(body.
|
|
227
|
+
expect(body['Dimensions@odata.bind']).toHaveLength(2);
|
|
228
|
+
expect(body['Dimensions@odata.bind'][0]).toBe("Dimensions('Time')");
|
|
229
|
+
expect(body['Dimensions@odata.bind'][1]).toBe("Dimensions('Account')");
|
|
230
|
+
});
|
|
231
|
+
test('should escape special characters in dimension names (parity with tm1py format_url)', () => {
|
|
232
|
+
const cube = new Cube_1.Cube('TestCube', ["O'Brien", '50%', 'A#B', 'X?Y', 'M&N']);
|
|
233
|
+
const body = JSON.parse(cube.body);
|
|
234
|
+
expect(body['Dimensions@odata.bind'][0]).toBe("Dimensions('O''Brien')");
|
|
235
|
+
expect(body['Dimensions@odata.bind'][1]).toBe("Dimensions('50%25')");
|
|
236
|
+
expect(body['Dimensions@odata.bind'][2]).toBe("Dimensions('A%23B')");
|
|
237
|
+
expect(body['Dimensions@odata.bind'][3]).toBe("Dimensions('X%3FY')");
|
|
238
|
+
expect(body['Dimensions@odata.bind'][4]).toBe("Dimensions('M%26N')");
|
|
216
239
|
});
|
|
217
240
|
test('should create cube from dictionary', () => {
|
|
218
241
|
const cubeDict = {
|
|
@@ -246,7 +269,7 @@ describe('Object Model - Improved Coverage', () => {
|
|
|
246
269
|
test('should add user to group', () => {
|
|
247
270
|
const user = new User_1.User('TestUser', []);
|
|
248
271
|
user.addGroup('NewGroup');
|
|
249
|
-
expect(user.groups).toContain('
|
|
272
|
+
expect(user.groups).toContain('NewGroup');
|
|
250
273
|
});
|
|
251
274
|
});
|
|
252
275
|
describe('View Object', () => {
|
|
@@ -289,13 +312,13 @@ describe('Object Model - Improved Coverage', () => {
|
|
|
289
312
|
const user = new User_1.User('BusinessUser', []);
|
|
290
313
|
// Add to basic group
|
|
291
314
|
user.addGroup('Everyone');
|
|
292
|
-
expect(user.groups).toContain('
|
|
315
|
+
expect(user.groups).toContain('Everyone');
|
|
293
316
|
// Promote to power user
|
|
294
317
|
user.addGroup('PowerUser');
|
|
295
318
|
expect(user.groups.length).toBeGreaterThan(1);
|
|
296
319
|
// Remove from basic group
|
|
297
320
|
user.removeGroup('Everyone');
|
|
298
|
-
expect(user.groups).toContain('
|
|
321
|
+
expect(user.groups).toContain('PowerUser');
|
|
299
322
|
});
|
|
300
323
|
});
|
|
301
324
|
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"user.issue61.test.d.ts","sourceRoot":"","sources":["../../src/tests/user.issue61.test.ts"],"names":[],"mappings":""}
|