@memberjunction/ng-whiteboard 5.42.0 → 5.44.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.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=whiteboard-agent-sees-popover.component.dom.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"whiteboard-agent-sees-popover.component.dom.test.d.ts","sourceRoot":"","sources":["../../src/lib/whiteboard-agent-sees-popover.component.dom.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,54 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { renderComponentFixture } from '@memberjunction/ng-test-utils';
3
+ import { RealtimeWhiteboardAgentSeesPopoverComponent } from './whiteboard-agent-sees-popover.component';
4
+ /**
5
+ * DOM spec for <mj-realtime-whiteboard-agent-sees-popover> — the "what the agent sees"
6
+ * perception popover. Covers the AgentName binding (appears in head + caption), the
7
+ * footer stat bindings (debounce / element count / last delta), and the @if/@else
8
+ * branch on TintedHtml: the empty placeholder when there is no delta, and the tinted
9
+ * <pre> when a delta is present (with the setter producing escaped, span-wrapped HTML).
10
+ */
11
+ describe('RealtimeWhiteboardAgentSeesPopoverComponent (DOM)', () => {
12
+ it('renders the AgentName in the head and the caption', () => {
13
+ const f = renderComponentFixture(RealtimeWhiteboardAgentSeesPopoverComponent, { inputs: { AgentName: 'Sage' } });
14
+ expect(f.nativeElement.querySelector('.sees-pop__head')?.textContent).toContain("Sage's context");
15
+ expect(f.nativeElement.querySelector('.sees-pop__cap')?.textContent).toContain('Sage');
16
+ });
17
+ it('renders the footer stats from DebounceMs / ElementCount / LastDeltaLabel', () => {
18
+ const f = renderComponentFixture(RealtimeWhiteboardAgentSeesPopoverComponent, {
19
+ inputs: { DebounceMs: 500, ElementCount: 7, LastDeltaLabel: '2 s ago' },
20
+ });
21
+ const foot = f.nativeElement.querySelector('.sees-pop__foot')?.textContent ?? '';
22
+ expect(foot).toContain('500 ms');
23
+ expect(foot).toContain('7');
24
+ expect(foot).toContain('2 s ago');
25
+ });
26
+ it('shows the "no deltas yet" placeholder when DeltaJson is empty', () => {
27
+ const f = renderComponentFixture(RealtimeWhiteboardAgentSeesPopoverComponent);
28
+ const code = f.nativeElement.querySelector('.codeblock')?.textContent ?? '';
29
+ expect(code).toContain('no deltas yet');
30
+ // the tinted-delta branch (which carries the replace-current-state comment) is absent
31
+ expect(code).not.toContain('replace-current-state');
32
+ });
33
+ it('renders the tinted delta block when DeltaJson is set', () => {
34
+ const f = renderComponentFixture(RealtimeWhiteboardAgentSeesPopoverComponent, {
35
+ inputs: { DeltaJson: '{"added": 3}' },
36
+ });
37
+ const pre = f.nativeElement.querySelector('.codeblock');
38
+ expect(pre).not.toBeNull();
39
+ // the @if-true branch carries the replace-current-state semantics comment
40
+ expect(pre.textContent).toContain('replace-current-state');
41
+ // the setter tints JSON keys (.ck) and numbers (.cn) into spans via [innerHTML]
42
+ expect(pre.querySelector('.ck')).not.toBeNull();
43
+ expect(pre.querySelector('.cn')).not.toBeNull();
44
+ });
45
+ it('escapes HTML in the delta JSON (no live markup is injected)', () => {
46
+ const f = renderComponentFixture(RealtimeWhiteboardAgentSeesPopoverComponent, {
47
+ inputs: { DeltaJson: '{"x": "<script>bad</script>"}' },
48
+ });
49
+ // the injected value is escaped, so no real <script> element exists in the popover
50
+ expect(f.nativeElement.querySelector('script')).toBeNull();
51
+ expect(f.nativeElement.querySelector('.codeblock')?.textContent).toContain('<script>');
52
+ });
53
+ });
54
+ //# sourceMappingURL=whiteboard-agent-sees-popover.component.dom.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"whiteboard-agent-sees-popover.component.dom.test.js","sourceRoot":"","sources":["../../src/lib/whiteboard-agent-sees-popover.component.dom.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it, expect } from 'vitest';\nimport { renderComponentFixture } from '@memberjunction/ng-test-utils';\nimport { RealtimeWhiteboardAgentSeesPopoverComponent } from './whiteboard-agent-sees-popover.component';\n\n/**\n * DOM spec for <mj-realtime-whiteboard-agent-sees-popover> — the \"what the agent sees\"\n * perception popover. Covers the AgentName binding (appears in head + caption), the\n * footer stat bindings (debounce / element count / last delta), and the @if/@else\n * branch on TintedHtml: the empty placeholder when there is no delta, and the tinted\n * <pre> when a delta is present (with the setter producing escaped, span-wrapped HTML).\n */\ndescribe('RealtimeWhiteboardAgentSeesPopoverComponent (DOM)', () => {\n it('renders the AgentName in the head and the caption', () => {\n const f = renderComponentFixture(RealtimeWhiteboardAgentSeesPopoverComponent, { inputs: { AgentName: 'Sage' } });\n expect(f.nativeElement.querySelector('.sees-pop__head')?.textContent).toContain(\"Sage's context\");\n expect(f.nativeElement.querySelector('.sees-pop__cap')?.textContent).toContain('Sage');\n });\n\n it('renders the footer stats from DebounceMs / ElementCount / LastDeltaLabel', () => {\n const f = renderComponentFixture(RealtimeWhiteboardAgentSeesPopoverComponent, {\n inputs: { DebounceMs: 500, ElementCount: 7, LastDeltaLabel: '2 s ago' },\n });\n const foot = f.nativeElement.querySelector('.sees-pop__foot')?.textContent ?? '';\n expect(foot).toContain('500 ms');\n expect(foot).toContain('7');\n expect(foot).toContain('2 s ago');\n });\n\n it('shows the \"no deltas yet\" placeholder when DeltaJson is empty', () => {\n const f = renderComponentFixture(RealtimeWhiteboardAgentSeesPopoverComponent);\n const code = f.nativeElement.querySelector('.codeblock')?.textContent ?? '';\n expect(code).toContain('no deltas yet');\n // the tinted-delta branch (which carries the replace-current-state comment) is absent\n expect(code).not.toContain('replace-current-state');\n });\n\n it('renders the tinted delta block when DeltaJson is set', () => {\n const f = renderComponentFixture(RealtimeWhiteboardAgentSeesPopoverComponent, {\n inputs: { DeltaJson: '{\"added\": 3}' },\n });\n const pre = f.nativeElement.querySelector('.codeblock');\n expect(pre).not.toBeNull();\n // the @if-true branch carries the replace-current-state semantics comment\n expect(pre!.textContent).toContain('replace-current-state');\n // the setter tints JSON keys (.ck) and numbers (.cn) into spans via [innerHTML]\n expect(pre!.querySelector('.ck')).not.toBeNull();\n expect(pre!.querySelector('.cn')).not.toBeNull();\n });\n\n it('escapes HTML in the delta JSON (no live markup is injected)', () => {\n const f = renderComponentFixture(RealtimeWhiteboardAgentSeesPopoverComponent, {\n inputs: { DeltaJson: '{\"x\": \"<script>bad</script>\"}' },\n });\n // the injected value is escaped, so no real <script> element exists in the popover\n expect(f.nativeElement.querySelector('script')).toBeNull();\n expect(f.nativeElement.querySelector('.codeblock')?.textContent).toContain('<script>');\n });\n});\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=whiteboard-pages.component.dom.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"whiteboard-pages.component.dom.test.d.ts","sourceRoot":"","sources":["../../src/lib/whiteboard-pages.component.dom.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,101 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { renderComponentFixture } from '@memberjunction/ng-test-utils';
3
+ import { RealtimeWhiteboardPagesComponent } from './whiteboard-pages.component';
4
+ import { WhiteboardState } from './whiteboard-state';
5
+ /**
6
+ * DOM spec for <mj-realtime-whiteboard-pages> — the OneNote-style page strip. This is a
7
+ * data-bound component, but its data source (WhiteboardState) is a plain Angular-free
8
+ * engine we can `new` directly — no metadata provider / RunView needed. Covers: a chip
9
+ * per page via @for, the [class.active] on the active page, the agent-authorship garnish,
10
+ * the @if(CanDelete) close-affordance gating (hidden on the lone page), the @if(!ReadOnly)
11
+ * add button, the ChipContextMenu @Output, and that clicking a chip / add button mutates
12
+ * the engine.
13
+ */
14
+ describe('RealtimeWhiteboardPagesComponent (DOM)', () => {
15
+ const chips = (f) => Array.from(f.nativeElement.querySelectorAll('.wb-page-chip'));
16
+ const renderWith = (state, readOnly = false) => renderComponentFixture(RealtimeWhiteboardPagesComponent, { inputs: { State: state, ReadOnly: readOnly } });
17
+ it('renders one chip per page in the engine', () => {
18
+ const state = new WhiteboardState();
19
+ state.AddPage('Second');
20
+ const f = renderWith(state);
21
+ expect(chips(f).length).toBe(2);
22
+ });
23
+ it('marks the active page chip with .active (and only one chip is active)', () => {
24
+ const state = new WhiteboardState();
25
+ state.AddPage('Second'); // AddPage switches to the new page, so it is active
26
+ const f = renderWith(state);
27
+ const active = chips(f).filter((c) => c.classList.contains('active'));
28
+ expect(active.length).toBe(1);
29
+ expect(active[0].querySelector('.wb-page-chip__name')?.textContent?.trim()).toBe('Second');
30
+ });
31
+ it('does NOT render the delete affordance when there is only one page', () => {
32
+ const state = new WhiteboardState();
33
+ const f = renderWith(state);
34
+ expect(f.nativeElement.querySelector('.wb-page-chip__close')).toBeNull();
35
+ });
36
+ it('renders a delete affordance on chips once there is more than one page', () => {
37
+ const state = new WhiteboardState();
38
+ state.AddPage('Second');
39
+ const f = renderWith(state);
40
+ expect(f.nativeElement.querySelectorAll('.wb-page-chip__close').length).toBe(2);
41
+ });
42
+ it('adds the agent garnish only to agent-authored page chips', () => {
43
+ const state = new WhiteboardState();
44
+ state.AddPage('Agent Page', 'agent');
45
+ const f = renderWith(state);
46
+ const agentChips = chips(f).filter((c) => c.classList.contains('wb-page-chip--agent'));
47
+ expect(agentChips.length).toBe(1);
48
+ expect(agentChips[0].querySelector('.wb-page-chip__agent-dot')).not.toBeNull();
49
+ });
50
+ it('renders the add (+) button when not read-only', () => {
51
+ const f = renderWith(new WhiteboardState(), false);
52
+ expect(f.nativeElement.querySelector('.wb-page-add')).not.toBeNull();
53
+ });
54
+ it('hides the add (+) button and delete affordances in read-only mode', () => {
55
+ const state = new WhiteboardState();
56
+ state.AddPage('Second');
57
+ const f = renderWith(state, true);
58
+ expect(f.nativeElement.querySelector('.wb-page-add')).toBeNull();
59
+ expect(f.nativeElement.querySelector('.wb-page-chip__close')).toBeNull();
60
+ });
61
+ it('switches the active page in the engine when an inactive chip is clicked', () => {
62
+ const state = new WhiteboardState();
63
+ const firstId = state.Pages[0].ID;
64
+ state.AddPage('Second'); // now "Second" is active, first is inactive
65
+ const f = renderWith(state);
66
+ // click the first (inactive) chip
67
+ const firstChip = chips(f).find((c) => c.querySelector('.wb-page-chip__name')?.textContent?.trim() === 'Page 1');
68
+ firstChip.click();
69
+ expect(state.ActivePageID).toBe(firstId);
70
+ });
71
+ it('adds a page to the engine when the + button is clicked', () => {
72
+ const state = new WhiteboardState();
73
+ const before = state.Pages.length;
74
+ const f = renderWith(state);
75
+ f.nativeElement.querySelector('.wb-page-add').click();
76
+ expect(state.Pages.length).toBe(before + 1);
77
+ });
78
+ it('emits ChipContextMenu (and suppresses the native menu) on right-click', () => {
79
+ const state = new WhiteboardState();
80
+ const spy = vi.fn();
81
+ const f = renderComponentFixture(RealtimeWhiteboardPagesComponent, {
82
+ inputs: { State: state, ReadOnly: false },
83
+ setup: (c) => c.ChipContextMenu.subscribe(spy),
84
+ });
85
+ const evt = new MouseEvent('contextmenu', { bubbles: true, cancelable: true });
86
+ chips(f)[0].dispatchEvent(evt);
87
+ expect(spy).toHaveBeenCalledTimes(1);
88
+ expect(evt.defaultPrevented).toBe(true);
89
+ });
90
+ it('does NOT emit ChipContextMenu in read-only mode', () => {
91
+ const state = new WhiteboardState();
92
+ const spy = vi.fn();
93
+ const f = renderComponentFixture(RealtimeWhiteboardPagesComponent, {
94
+ inputs: { State: state, ReadOnly: true },
95
+ setup: (c) => c.ChipContextMenu.subscribe(spy),
96
+ });
97
+ chips(f)[0].dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true }));
98
+ expect(spy).not.toHaveBeenCalled();
99
+ });
100
+ });
101
+ //# sourceMappingURL=whiteboard-pages.component.dom.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"whiteboard-pages.component.dom.test.js","sourceRoot":"","sources":["../../src/lib/whiteboard-pages.component.dom.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it, expect, vi } from 'vitest';\nimport { renderComponentFixture } from '@memberjunction/ng-test-utils';\nimport { RealtimeWhiteboardPagesComponent } from './whiteboard-pages.component';\nimport { WhiteboardState } from './whiteboard-state';\n\n/**\n * DOM spec for <mj-realtime-whiteboard-pages> — the OneNote-style page strip. This is a\n * data-bound component, but its data source (WhiteboardState) is a plain Angular-free\n * engine we can `new` directly — no metadata provider / RunView needed. Covers: a chip\n * per page via @for, the [class.active] on the active page, the agent-authorship garnish,\n * the @if(CanDelete) close-affordance gating (hidden on the lone page), the @if(!ReadOnly)\n * add button, the ChipContextMenu @Output, and that clicking a chip / add button mutates\n * the engine.\n */\ndescribe('RealtimeWhiteboardPagesComponent (DOM)', () => {\n type Fix = ReturnType<typeof renderComponentFixture<RealtimeWhiteboardPagesComponent>>;\n const chips = (f: Fix) => Array.from(f.nativeElement.querySelectorAll('.wb-page-chip')) as HTMLButtonElement[];\n\n const renderWith = (state: WhiteboardState, readOnly = false): Fix =>\n renderComponentFixture(RealtimeWhiteboardPagesComponent, { inputs: { State: state, ReadOnly: readOnly } });\n\n it('renders one chip per page in the engine', () => {\n const state = new WhiteboardState();\n state.AddPage('Second');\n const f = renderWith(state);\n expect(chips(f).length).toBe(2);\n });\n\n it('marks the active page chip with .active (and only one chip is active)', () => {\n const state = new WhiteboardState();\n state.AddPage('Second'); // AddPage switches to the new page, so it is active\n const f = renderWith(state);\n const active = chips(f).filter((c) => c.classList.contains('active'));\n expect(active.length).toBe(1);\n expect(active[0].querySelector('.wb-page-chip__name')?.textContent?.trim()).toBe('Second');\n });\n\n it('does NOT render the delete affordance when there is only one page', () => {\n const state = new WhiteboardState();\n const f = renderWith(state);\n expect(f.nativeElement.querySelector('.wb-page-chip__close')).toBeNull();\n });\n\n it('renders a delete affordance on chips once there is more than one page', () => {\n const state = new WhiteboardState();\n state.AddPage('Second');\n const f = renderWith(state);\n expect(f.nativeElement.querySelectorAll('.wb-page-chip__close').length).toBe(2);\n });\n\n it('adds the agent garnish only to agent-authored page chips', () => {\n const state = new WhiteboardState();\n state.AddPage('Agent Page', 'agent');\n const f = renderWith(state);\n const agentChips = chips(f).filter((c) => c.classList.contains('wb-page-chip--agent'));\n expect(agentChips.length).toBe(1);\n expect(agentChips[0].querySelector('.wb-page-chip__agent-dot')).not.toBeNull();\n });\n\n it('renders the add (+) button when not read-only', () => {\n const f = renderWith(new WhiteboardState(), false);\n expect(f.nativeElement.querySelector('.wb-page-add')).not.toBeNull();\n });\n\n it('hides the add (+) button and delete affordances in read-only mode', () => {\n const state = new WhiteboardState();\n state.AddPage('Second');\n const f = renderWith(state, true);\n expect(f.nativeElement.querySelector('.wb-page-add')).toBeNull();\n expect(f.nativeElement.querySelector('.wb-page-chip__close')).toBeNull();\n });\n\n it('switches the active page in the engine when an inactive chip is clicked', () => {\n const state = new WhiteboardState();\n const firstId = state.Pages[0].ID;\n state.AddPage('Second'); // now \"Second\" is active, first is inactive\n const f = renderWith(state);\n // click the first (inactive) chip\n const firstChip = chips(f).find((c) => c.querySelector('.wb-page-chip__name')?.textContent?.trim() === 'Page 1')!;\n firstChip.click();\n expect(state.ActivePageID).toBe(firstId);\n });\n\n it('adds a page to the engine when the + button is clicked', () => {\n const state = new WhiteboardState();\n const before = state.Pages.length;\n const f = renderWith(state);\n (f.nativeElement.querySelector('.wb-page-add') as HTMLButtonElement).click();\n expect(state.Pages.length).toBe(before + 1);\n });\n\n it('emits ChipContextMenu (and suppresses the native menu) on right-click', () => {\n const state = new WhiteboardState();\n const spy = vi.fn();\n const f = renderComponentFixture(RealtimeWhiteboardPagesComponent, {\n inputs: { State: state, ReadOnly: false },\n setup: (c) => c.ChipContextMenu.subscribe(spy),\n });\n const evt = new MouseEvent('contextmenu', { bubbles: true, cancelable: true });\n chips(f)[0].dispatchEvent(evt);\n expect(spy).toHaveBeenCalledTimes(1);\n expect(evt.defaultPrevented).toBe(true);\n });\n\n it('does NOT emit ChipContextMenu in read-only mode', () => {\n const state = new WhiteboardState();\n const spy = vi.fn();\n const f = renderComponentFixture(RealtimeWhiteboardPagesComponent, {\n inputs: { State: state, ReadOnly: true },\n setup: (c) => c.ChipContextMenu.subscribe(spy),\n });\n chips(f)[0].dispatchEvent(new MouseEvent('contextmenu', { bubbles: true, cancelable: true }));\n expect(spy).not.toHaveBeenCalled();\n });\n});\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"whiteboard-snapshot.component.d.ts","sourceRoot":"","sources":["../../src/lib/whiteboard-snapshot.component.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;;AAErD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,eAAe,GAAG,IAAI,CAS3F;AAED;;;;;;;GAOG;AACH,qBAgDa,2BAA2B;IACtC,OAAO,CAAC,UAAU,CAAM;IAExB,0EAA0E;IACnE,KAAK,EAAE,eAAe,GAAG,IAAI,CAAQ;IAE5C,mFAAmF;IACnF,IACI,SAAS,CAAC,KAAK,EAAE,MAAM,EAK1B;IACD,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED,mFAAmF;IAC1E,SAAS,SAAW;yCAnBlB,2BAA2B;2CAA3B,2BAA2B;CAoBvC"}
1
+ {"version":3,"file":"whiteboard-snapshot.component.d.ts","sourceRoot":"","sources":["../../src/lib/whiteboard-snapshot.component.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;;AAGrD;;;;;GAKG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAAG,eAAe,GAAG,IAAI,CAS3F;AAED;;;;;;;GAOG;AACH,qBA+Ca,2BAA2B;IACtC,OAAO,CAAC,UAAU,CAAM;IAExB,0EAA0E;IACnE,KAAK,EAAE,eAAe,GAAG,IAAI,CAAQ;IAE5C,mFAAmF;IACnF,IACI,SAAS,CAAC,KAAK,EAAE,MAAM,EAK1B;IACD,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED,mFAAmF;IAC1E,SAAS,SAAW;yCAnBlB,2BAA2B;2CAA3B,2BAA2B;CAoBvC"}
@@ -1,6 +1,7 @@
1
1
  import { Component, Input } from '@angular/core';
2
2
  import { RealtimeWhiteboardBoardComponent } from './whiteboard-board.component';
3
3
  import { WhiteboardState } from './whiteboard-state';
4
+ import { MJEmptyStateComponent } from '@memberjunction/ng-ui-components';
4
5
  import * as i0 from "@angular/core";
5
6
  function WhiteboardSnapshotComponent_Conditional_0_Template(rf, ctx) { if (rf & 1) {
6
7
  i0.ɵɵelementStart(0, "div", 0);
@@ -12,11 +13,7 @@ function WhiteboardSnapshotComponent_Conditional_0_Template(rf, ctx) { if (rf &
12
13
  i0.ɵɵproperty("State", ctx_r0.State)("AgentName", ctx_r0.AgentName)("ReadOnly", true)("Tool", "pan");
13
14
  } }
14
15
  function WhiteboardSnapshotComponent_Conditional_1_Template(rf, ctx) { if (rf & 1) {
15
- i0.ɵɵelementStart(0, "div", 1);
16
- i0.ɵɵelement(1, "i", 3);
17
- i0.ɵɵelementStart(2, "span");
18
- i0.ɵɵtext(3, "No board content");
19
- i0.ɵɵelementEnd()();
16
+ i0.ɵɵelement(0, "mj-empty-state", 1);
20
17
  } }
21
18
  /**
22
19
  * Parse a persisted whiteboard payload ({@link WhiteboardState.ToJSON} output) into a
@@ -62,24 +59,23 @@ export class WhiteboardSnapshotComponent {
62
59
  return this._stateJson;
63
60
  }
64
61
  static { this.ɵfac = function WhiteboardSnapshotComponent_Factory(__ngFactoryType__) { return new (__ngFactoryType__ || WhiteboardSnapshotComponent)(); }; }
65
- static { this.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: WhiteboardSnapshotComponent, selectors: [["mj-whiteboard-snapshot"]], inputs: { StateJson: "StateJson", AgentName: "AgentName" }, decls: 2, vars: 1, consts: [[1, "wb-snapshot-board"], [1, "wb-snapshot-empty"], [3, "State", "AgentName", "ReadOnly", "Tool"], ["aria-hidden", "true", 1, "fa-solid", "fa-chalkboard"]], template: function WhiteboardSnapshotComponent_Template(rf, ctx) { if (rf & 1) {
66
- i0.ɵɵconditionalCreate(0, WhiteboardSnapshotComponent_Conditional_0_Template, 2, 4, "div", 0)(1, WhiteboardSnapshotComponent_Conditional_1_Template, 4, 0, "div", 1);
62
+ static { this.ɵcmp = /*@__PURE__*/ i0.ɵɵdefineComponent({ type: WhiteboardSnapshotComponent, selectors: [["mj-whiteboard-snapshot"]], inputs: { StateJson: "StateJson", AgentName: "AgentName" }, decls: 2, vars: 1, consts: [[1, "wb-snapshot-board"], ["Size", "compact", "Icon", "fa-solid fa-chalkboard", "Title", "No board content", 1, "wb-snapshot-empty"], [3, "State", "AgentName", "ReadOnly", "Tool"]], template: function WhiteboardSnapshotComponent_Template(rf, ctx) { if (rf & 1) {
63
+ i0.ɵɵconditionalCreate(0, WhiteboardSnapshotComponent_Conditional_0_Template, 2, 4, "div", 0)(1, WhiteboardSnapshotComponent_Conditional_1_Template, 1, 0, "mj-empty-state", 1);
67
64
  } if (rf & 2) {
68
65
  i0.ɵɵconditional(ctx.State ? 0 : 1);
69
- } }, dependencies: [RealtimeWhiteboardBoardComponent], styles: ["[_nghost-%COMP%] {\n display: flex;\n flex-direction: column;\n flex: 1;\n height: 100%;\n min-height: 320px;\n }\n \n\n .wb-snapshot-board[_ngcontent-%COMP%] {\n position: relative;\n flex: 1;\n min-height: 0;\n }\n .wb-snapshot-empty[_ngcontent-%COMP%] {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 8px;\n color: var(--mj-text-muted);\n background: var(--mj-bg-surface-sunken);\n border-radius: var(--mj-radius-md, 6px);\n font-size: 13px;\n }\n .wb-snapshot-empty[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: 22px;\n color: var(--mj-text-disabled);\n }"] }); }
66
+ } }, dependencies: [RealtimeWhiteboardBoardComponent, MJEmptyStateComponent], styles: ["[_nghost-%COMP%] {\n display: flex;\n flex-direction: column;\n flex: 1;\n height: 100%;\n min-height: 320px;\n }\n \n\n .wb-snapshot-board[_ngcontent-%COMP%] {\n position: relative;\n flex: 1;\n min-height: 0;\n }\n .wb-snapshot-empty[_ngcontent-%COMP%] {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 8px;\n color: var(--mj-text-muted);\n background: var(--mj-bg-surface-sunken);\n border-radius: var(--mj-radius-md, 6px);\n font-size: 13px;\n }\n .wb-snapshot-empty[_ngcontent-%COMP%] i[_ngcontent-%COMP%] {\n font-size: 22px;\n color: var(--mj-text-disabled);\n }"] }); }
70
67
  }
71
68
  (() => { (typeof ngDevMode === "undefined" || ngDevMode) && i0.ɵsetClassMetadata(WhiteboardSnapshotComponent, [{
72
69
  type: Component,
73
- args: [{ standalone: true, selector: 'mj-whiteboard-snapshot', imports: [RealtimeWhiteboardBoardComponent], template: `
70
+ args: [{ standalone: true, selector: 'mj-whiteboard-snapshot', imports: [RealtimeWhiteboardBoardComponent, MJEmptyStateComponent], template: `
74
71
  @if (State) {
75
72
  <div class="wb-snapshot-board">
76
73
  <mj-realtime-whiteboard [State]="State" [AgentName]="AgentName" [ReadOnly]="true" [Tool]="'pan'" />
77
74
  </div>
78
75
  } @else {
79
- <div class="wb-snapshot-empty">
80
- <i class="fa-solid fa-chalkboard" aria-hidden="true"></i>
81
- <span>No board content</span>
82
- </div>
76
+ <mj-empty-state class="wb-snapshot-empty" Size="compact"
77
+ Icon="fa-solid fa-chalkboard"
78
+ Title="No board content" />
83
79
  }
84
80
  `, styles: ["\n :host {\n display: flex;\n flex-direction: column;\n flex: 1;\n height: 100%;\n min-height: 320px;\n }\n /* the board component positions itself absolute, so give it a relative frame */\n .wb-snapshot-board {\n position: relative;\n flex: 1;\n min-height: 0;\n }\n .wb-snapshot-empty {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 8px;\n color: var(--mj-text-muted);\n background: var(--mj-bg-surface-sunken);\n border-radius: var(--mj-radius-md, 6px);\n font-size: 13px;\n }\n .wb-snapshot-empty i {\n font-size: 22px;\n color: var(--mj-text-disabled);\n }\n "] }]
85
81
  }], null, { StateJson: [{
@@ -1 +1 @@
1
- {"version":3,"file":"whiteboard-snapshot.component.js","sourceRoot":"","sources":["../../src/lib/whiteboard-snapshot.component.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,gCAAgC,EAAE,MAAM,8BAA8B,CAAC;AAChF,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;;;IAiC/C,8BAA+B;IAC7B,4CAAmG;IACrG,iBAAM;;;IADoB,cAAe;IAA2C,AAAlB,AAAxB,AAAhB,oCAAe,+BAAwB,kBAAkB,eAAe;;;IAGlG,8BAA+B;IAC7B,uBAAyD;IACzD,4BAAM;IAAA,gCAAgB;IACxB,AADwB,iBAAO,EACzB;;AAtCZ;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAA+B;IACjE,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtC,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,CAAC;QACH,OAAO,eAAe,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACxC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AAiDH,MAAM,OAAO,2BAA2B;IAhDxC;QAiDU,eAAU,GAAG,EAAE,CAAC;QAExB,0EAA0E;QACnE,UAAK,GAA2B,IAAI,CAAC;QAc5C,mFAAmF;QAC1E,cAAS,GAAG,OAAO,CAAC;KAC9B;IAdC,mFAAmF;IACnF,IACI,SAAS,CAAC,KAAa;QACzB,IAAI,KAAK,KAAK,IAAI,CAAC,UAAU,EAAE,CAAC;YAC9B,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC;YACxB,IAAI,CAAC,KAAK,GAAG,mBAAmB,CAAC,KAAK,CAAC,CAAC;QAC1C,CAAC;IACH,CAAC;IACD,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,UAAU,CAAC;IACzB,CAAC;4HAhBU,2BAA2B;oEAA3B,2BAA2B;YAvClC,AAJF,6FAAa,uEAIJ;;YAJT,mCASC;4BAXO,gCAAgC;;iFA6C/B,2BAA2B;cAhDvC,SAAS;6BACI,IAAI,YACN,wBAAwB,WACzB,CAAC,gCAAgC,CAAC,YACjC;;;;;;;;;;;GAWT;;kBAwCA,KAAK;;kBAYL,KAAK;;kFAnBK,2BAA2B","sourcesContent":["import { Component, Input } from '@angular/core';\nimport { RealtimeWhiteboardBoardComponent } from './whiteboard-board.component';\nimport { WhiteboardState } from './whiteboard-state';\n\n/**\n * Parse a persisted whiteboard payload ({@link WhiteboardState.ToJSON} output) into a\n * rehydrated engine instance. PURE + TOLERANT: any malformed input (empty, non-JSON,\n * JSON of the wrong shape) returns `null` instead of throwing — callers render an\n * empty state rather than breaking the surface they're embedded in.\n */\nexport function ParseBoardStateJson(json: string | null | undefined): WhiteboardState | null {\n if (!json || json.trim().length === 0) {\n return null;\n }\n try {\n return WhiteboardState.FromJSON(json);\n } catch {\n return null;\n }\n}\n\n/**\n * READ-ONLY SNAPSHOT of a whiteboard — renders a persisted board payload (the\n * session-channel artifact JSON) through the real board component in `ReadOnly` mode,\n * so a saved board looks exactly like the live one: same chips, shapes, ink and\n * connectors, with pan + zoom still available for navigation but every mutating\n * interaction disabled. Used by the whiteboard artifact viewer plugin and any\n * session-review surface that wants to show a board without an engine wired to tools.\n */\n@Component({\n standalone: true,\n selector: 'mj-whiteboard-snapshot',\n imports: [RealtimeWhiteboardBoardComponent],\n template: `\n @if (State) {\n <div class=\"wb-snapshot-board\">\n <mj-realtime-whiteboard [State]=\"State\" [AgentName]=\"AgentName\" [ReadOnly]=\"true\" [Tool]=\"'pan'\" />\n </div>\n } @else {\n <div class=\"wb-snapshot-empty\">\n <i class=\"fa-solid fa-chalkboard\" aria-hidden=\"true\"></i>\n <span>No board content</span>\n </div>\n }\n `,\n styles: [`\n :host {\n display: flex;\n flex-direction: column;\n flex: 1;\n height: 100%;\n min-height: 320px;\n }\n /* the board component positions itself absolute, so give it a relative frame */\n .wb-snapshot-board {\n position: relative;\n flex: 1;\n min-height: 0;\n }\n .wb-snapshot-empty {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 8px;\n color: var(--mj-text-muted);\n background: var(--mj-bg-surface-sunken);\n border-radius: var(--mj-radius-md, 6px);\n font-size: 13px;\n }\n .wb-snapshot-empty i {\n font-size: 22px;\n color: var(--mj-text-disabled);\n }\n `]\n})\nexport class WhiteboardSnapshotComponent {\n private _stateJson = '';\n\n /** Rehydrated board engine (null when the payload is empty/malformed). */\n public State: WhiteboardState | null = null;\n\n /** Persisted board payload — setter-driven so a new artifact version re-parses. */\n @Input()\n set StateJson(value: string) {\n if (value !== this._stateJson) {\n this._stateJson = value;\n this.State = ParseBoardStateJson(value);\n }\n }\n get StateJson(): string {\n return this._stateJson;\n }\n\n /** Display name of the session's agent — ownership chips on the rendered board. */\n @Input() AgentName = 'Agent';\n}\n"]}
1
+ {"version":3,"file":"whiteboard-snapshot.component.js","sourceRoot":"","sources":["../../src/lib/whiteboard-snapshot.component.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,eAAe,CAAC;AACjD,OAAO,EAAE,gCAAgC,EAAE,MAAM,8BAA8B,CAAC;AAChF,OAAO,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAC;AACrD,OAAO,EAAE,qBAAqB,EAAE,MAAM,kCAAkC,CAAC;;;IAiCnE,8BAA+B;IAC7B,4CAAmG;IACrG,iBAAM;;;IADoB,cAAe;IAA2C,AAAlB,AAAxB,AAAhB,oCAAe,+BAAwB,kBAAkB,eAAe;;;IAGlG,oCAE6B;;AArCnC;;;;;GAKG;AACH,MAAM,UAAU,mBAAmB,CAAC,IAA+B;IACjE,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACtC,OAAO,IAAI,CAAC;IACd,CAAC;IACD,IAAI,CAAC;QACH,OAAO,eAAe,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;IACxC,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED;;;;;;;GAOG;AAgDH,MAAM,OAAO,2BAA2B;IA/CxC;QAgDU,eAAU,GAAG,EAAE,CAAC;QAExB,0EAA0E;QACnE,UAAK,GAA2B,IAAI,CAAC;QAc5C,mFAAmF;QAC1E,cAAS,GAAG,OAAO,CAAC;KAC9B;IAdC,mFAAmF;IACnF,IACI,SAAS,CAAC,KAAa;QACzB,IAAI,KAAK,KAAK,IAAI,CAAC,UAAU,EAAE,CAAC;YAC9B,IAAI,CAAC,UAAU,GAAG,KAAK,CAAC;YACxB,IAAI,CAAC,KAAK,GAAG,mBAAmB,CAAC,KAAK,CAAC,CAAC;QAC1C,CAAC;IACH,CAAC;IACD,IAAI,SAAS;QACX,OAAO,IAAI,CAAC,UAAU,CAAC;IACzB,CAAC;4HAhBU,2BAA2B;oEAA3B,2BAA2B;YAtClC,AAJF,6FAAa,kFAIJ;;YAJT,mCAQC;4BAVO,gCAAgC,EAAE,qBAAqB;;iFA4CtD,2BAA2B;cA/CvC,SAAS;6BACI,IAAI,YACN,wBAAwB,WACzB,CAAC,gCAAgC,EAAE,qBAAqB,CAAC,YACxD;;;;;;;;;;GAUT;;kBAwCA,KAAK;;kBAYL,KAAK;;kFAnBK,2BAA2B","sourcesContent":["import { Component, Input } from '@angular/core';\nimport { RealtimeWhiteboardBoardComponent } from './whiteboard-board.component';\nimport { WhiteboardState } from './whiteboard-state';\nimport { MJEmptyStateComponent } from '@memberjunction/ng-ui-components';\n\n/**\n * Parse a persisted whiteboard payload ({@link WhiteboardState.ToJSON} output) into a\n * rehydrated engine instance. PURE + TOLERANT: any malformed input (empty, non-JSON,\n * JSON of the wrong shape) returns `null` instead of throwing — callers render an\n * empty state rather than breaking the surface they're embedded in.\n */\nexport function ParseBoardStateJson(json: string | null | undefined): WhiteboardState | null {\n if (!json || json.trim().length === 0) {\n return null;\n }\n try {\n return WhiteboardState.FromJSON(json);\n } catch {\n return null;\n }\n}\n\n/**\n * READ-ONLY SNAPSHOT of a whiteboard — renders a persisted board payload (the\n * session-channel artifact JSON) through the real board component in `ReadOnly` mode,\n * so a saved board looks exactly like the live one: same chips, shapes, ink and\n * connectors, with pan + zoom still available for navigation but every mutating\n * interaction disabled. Used by the whiteboard artifact viewer plugin and any\n * session-review surface that wants to show a board without an engine wired to tools.\n */\n@Component({\n standalone: true,\n selector: 'mj-whiteboard-snapshot',\n imports: [RealtimeWhiteboardBoardComponent, MJEmptyStateComponent],\n template: `\n @if (State) {\n <div class=\"wb-snapshot-board\">\n <mj-realtime-whiteboard [State]=\"State\" [AgentName]=\"AgentName\" [ReadOnly]=\"true\" [Tool]=\"'pan'\" />\n </div>\n } @else {\n <mj-empty-state class=\"wb-snapshot-empty\" Size=\"compact\"\n Icon=\"fa-solid fa-chalkboard\"\n Title=\"No board content\" />\n }\n `,\n styles: [`\n :host {\n display: flex;\n flex-direction: column;\n flex: 1;\n height: 100%;\n min-height: 320px;\n }\n /* the board component positions itself absolute, so give it a relative frame */\n .wb-snapshot-board {\n position: relative;\n flex: 1;\n min-height: 0;\n }\n .wb-snapshot-empty {\n flex: 1;\n display: flex;\n flex-direction: column;\n align-items: center;\n justify-content: center;\n gap: 8px;\n color: var(--mj-text-muted);\n background: var(--mj-bg-surface-sunken);\n border-radius: var(--mj-radius-md, 6px);\n font-size: 13px;\n }\n .wb-snapshot-empty i {\n font-size: 22px;\n color: var(--mj-text-disabled);\n }\n `]\n})\nexport class WhiteboardSnapshotComponent {\n private _stateJson = '';\n\n /** Rehydrated board engine (null when the payload is empty/malformed). */\n public State: WhiteboardState | null = null;\n\n /** Persisted board payload — setter-driven so a new artifact version re-parses. */\n @Input()\n set StateJson(value: string) {\n if (value !== this._stateJson) {\n this._stateJson = value;\n this.State = ParseBoardStateJson(value);\n }\n }\n get StateJson(): string {\n return this._stateJson;\n }\n\n /** Display name of the session's agent — ownership chips on the rendered board. */\n @Input() AgentName = 'Agent';\n}\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=whiteboard-toolbar.component.dom.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"whiteboard-toolbar.component.dom.test.d.ts","sourceRoot":"","sources":["../../src/lib/whiteboard-toolbar.component.dom.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,108 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { renderComponentFixture } from '@memberjunction/ng-test-utils';
3
+ import { RealtimeWhiteboardToolbarComponent } from './whiteboard-toolbar.component';
4
+ /**
5
+ * DOM spec for <mj-realtime-whiteboard-toolbar> — the floating left-edge tool palette.
6
+ * Covers the @for tool rendering, [class.active] on the active tool, the @if pen/text/
7
+ * shape flyouts that only render when their tool is active, the undo/redo [disabled]
8
+ * gating, and the @Output emissions (tool change, pen color/width, undo/redo, and the
9
+ * compound text-style picks that emit BOTH a *Change and StyleSelection).
10
+ */
11
+ describe('RealtimeWhiteboardToolbarComponent (DOM)', () => {
12
+ const toolButtons = (f) => Array.from(f.nativeElement.querySelectorAll('button.tool'));
13
+ it('renders one tool button per entry in Tools (plus undo + redo)', () => {
14
+ const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent);
15
+ // 11 tools + undo + redo = 13 .tool buttons
16
+ expect(toolButtons(f).length).toBe(f.componentInstance.Tools.length + 2);
17
+ });
18
+ it('marks the active tool button with the .active class and only that one', () => {
19
+ const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent, { inputs: { ActiveTool: 'pen' } });
20
+ const active = toolButtons(f).filter((b) => b.classList.contains('active'));
21
+ expect(active.length).toBe(1);
22
+ expect(active[0].getAttribute('title')).toBe('Pen');
23
+ });
24
+ it('does not render the pen flyout when pen is not the active tool', () => {
25
+ const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent, { inputs: { ActiveTool: 'select' } });
26
+ expect(f.nativeElement.querySelector('.pen-flyout')).toBeNull();
27
+ });
28
+ it('renders the pen flyout (with one swatch per pen color) when pen is active', () => {
29
+ const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent, { inputs: { ActiveTool: 'pen' } });
30
+ const flyout = f.nativeElement.querySelector('.pen-flyout');
31
+ expect(flyout).not.toBeNull();
32
+ expect(flyout.querySelectorAll('.pen-c').length).toBe(f.componentInstance.PenColors.length);
33
+ });
34
+ it('shows the agent name in the pen-flyout violet note', () => {
35
+ const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent, { inputs: { ActiveTool: 'pen', AgentName: 'Sage' } });
36
+ expect(f.nativeElement.querySelector('.pen-note')?.textContent).toContain("violet = Sage's");
37
+ });
38
+ it('renders the text-style flyout only when text is the active tool', () => {
39
+ const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent, { inputs: { ActiveTool: 'text' } });
40
+ expect(f.nativeElement.querySelector('.text-flyout')).not.toBeNull();
41
+ });
42
+ it('renders the shape flyout only when shape is the active tool', () => {
43
+ const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent, { inputs: { ActiveTool: 'shape' } });
44
+ const flyout = f.nativeElement.querySelector('.shape-flyout');
45
+ expect(flyout).not.toBeNull();
46
+ expect(flyout.querySelectorAll('.shape-c').length).toBe(f.componentInstance.ShapeKinds.length);
47
+ });
48
+ it('disables undo/redo by default and enables them when CanUndo/CanRedo are true', () => {
49
+ const disabled = renderComponentFixture(RealtimeWhiteboardToolbarComponent);
50
+ const undoD = disabled.nativeElement.querySelector('button[title="Undo"]');
51
+ const redoD = disabled.nativeElement.querySelector('button[title="Redo"]');
52
+ expect(undoD.disabled).toBe(true);
53
+ expect(redoD.disabled).toBe(true);
54
+ const enabled = renderComponentFixture(RealtimeWhiteboardToolbarComponent, { inputs: { CanUndo: true, CanRedo: true } });
55
+ expect(enabled.nativeElement.querySelector('button[title="Undo"]').disabled).toBe(false);
56
+ expect(enabled.nativeElement.querySelector('button[title="Redo"]').disabled).toBe(false);
57
+ });
58
+ it('emits ToolChange with the clicked tool', () => {
59
+ const spy = vi.fn();
60
+ const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent, { setup: (c) => c.ToolChange.subscribe(spy) });
61
+ // the pen tool button (3rd entry, index 2)
62
+ const penBtn = f.nativeElement.querySelector('button[title="Pen"]');
63
+ penBtn.click();
64
+ expect(spy).toHaveBeenCalledWith('pen');
65
+ });
66
+ it('emits PenColorChange when a pen swatch is clicked', () => {
67
+ const spy = vi.fn();
68
+ const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent, {
69
+ inputs: { ActiveTool: 'pen' },
70
+ setup: (c) => c.PenColorChange.subscribe(spy),
71
+ });
72
+ const swatch = f.nativeElement.querySelector('.pen-flyout .pen-c');
73
+ swatch.click();
74
+ expect(spy).toHaveBeenCalledWith(f.componentInstance.PenColors[0]);
75
+ });
76
+ it('emits BOTH TextSizeChange and StyleSelection when a text size is picked', () => {
77
+ const sizeSpy = vi.fn();
78
+ const styleSpy = vi.fn();
79
+ const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent, {
80
+ inputs: { ActiveTool: 'text' },
81
+ setup: (c) => {
82
+ c.TextSizeChange.subscribe(sizeSpy);
83
+ c.StyleSelection.subscribe(styleSpy);
84
+ },
85
+ });
86
+ const sizeEl = f.nativeElement.querySelector('.text-flyout .txt-s');
87
+ sizeEl.click();
88
+ const firstSize = f.componentInstance.TextSizes[0];
89
+ expect(sizeSpy).toHaveBeenCalledWith(firstSize);
90
+ expect(styleSpy).toHaveBeenCalledWith({ FontSize: firstSize });
91
+ });
92
+ it('emits Undo / Redo when those buttons are enabled and clicked', () => {
93
+ const undoSpy = vi.fn();
94
+ const redoSpy = vi.fn();
95
+ const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent, {
96
+ inputs: { CanUndo: true, CanRedo: true },
97
+ setup: (c) => {
98
+ c.Undo.subscribe(undoSpy);
99
+ c.Redo.subscribe(redoSpy);
100
+ },
101
+ });
102
+ f.nativeElement.querySelector('button[title="Undo"]').click();
103
+ f.nativeElement.querySelector('button[title="Redo"]').click();
104
+ expect(undoSpy).toHaveBeenCalledTimes(1);
105
+ expect(redoSpy).toHaveBeenCalledTimes(1);
106
+ });
107
+ });
108
+ //# sourceMappingURL=whiteboard-toolbar.component.dom.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"whiteboard-toolbar.component.dom.test.js","sourceRoot":"","sources":["../../src/lib/whiteboard-toolbar.component.dom.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it, expect, vi } from 'vitest';\nimport { renderComponentFixture } from '@memberjunction/ng-test-utils';\nimport { RealtimeWhiteboardToolbarComponent } from './whiteboard-toolbar.component';\n\n/**\n * DOM spec for <mj-realtime-whiteboard-toolbar> — the floating left-edge tool palette.\n * Covers the @for tool rendering, [class.active] on the active tool, the @if pen/text/\n * shape flyouts that only render when their tool is active, the undo/redo [disabled]\n * gating, and the @Output emissions (tool change, pen color/width, undo/redo, and the\n * compound text-style picks that emit BOTH a *Change and StyleSelection).\n */\ndescribe('RealtimeWhiteboardToolbarComponent (DOM)', () => {\n type Fix = ReturnType<typeof renderComponentFixture<RealtimeWhiteboardToolbarComponent>>;\n const toolButtons = (f: Fix) => Array.from(f.nativeElement.querySelectorAll('button.tool')) as HTMLButtonElement[];\n\n it('renders one tool button per entry in Tools (plus undo + redo)', () => {\n const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent);\n // 11 tools + undo + redo = 13 .tool buttons\n expect(toolButtons(f).length).toBe(f.componentInstance.Tools.length + 2);\n });\n\n it('marks the active tool button with the .active class and only that one', () => {\n const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent, { inputs: { ActiveTool: 'pen' } });\n const active = toolButtons(f).filter((b) => b.classList.contains('active'));\n expect(active.length).toBe(1);\n expect(active[0].getAttribute('title')).toBe('Pen');\n });\n\n it('does not render the pen flyout when pen is not the active tool', () => {\n const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent, { inputs: { ActiveTool: 'select' } });\n expect(f.nativeElement.querySelector('.pen-flyout')).toBeNull();\n });\n\n it('renders the pen flyout (with one swatch per pen color) when pen is active', () => {\n const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent, { inputs: { ActiveTool: 'pen' } });\n const flyout = f.nativeElement.querySelector('.pen-flyout');\n expect(flyout).not.toBeNull();\n expect(flyout!.querySelectorAll('.pen-c').length).toBe(f.componentInstance.PenColors.length);\n });\n\n it('shows the agent name in the pen-flyout violet note', () => {\n const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent, { inputs: { ActiveTool: 'pen', AgentName: 'Sage' } });\n expect(f.nativeElement.querySelector('.pen-note')?.textContent).toContain(\"violet = Sage's\");\n });\n\n it('renders the text-style flyout only when text is the active tool', () => {\n const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent, { inputs: { ActiveTool: 'text' } });\n expect(f.nativeElement.querySelector('.text-flyout')).not.toBeNull();\n });\n\n it('renders the shape flyout only when shape is the active tool', () => {\n const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent, { inputs: { ActiveTool: 'shape' } });\n const flyout = f.nativeElement.querySelector('.shape-flyout');\n expect(flyout).not.toBeNull();\n expect(flyout!.querySelectorAll('.shape-c').length).toBe(f.componentInstance.ShapeKinds.length);\n });\n\n it('disables undo/redo by default and enables them when CanUndo/CanRedo are true', () => {\n const disabled = renderComponentFixture(RealtimeWhiteboardToolbarComponent);\n const undoD = disabled.nativeElement.querySelector('button[title=\"Undo\"]') as HTMLButtonElement;\n const redoD = disabled.nativeElement.querySelector('button[title=\"Redo\"]') as HTMLButtonElement;\n expect(undoD.disabled).toBe(true);\n expect(redoD.disabled).toBe(true);\n\n const enabled = renderComponentFixture(RealtimeWhiteboardToolbarComponent, { inputs: { CanUndo: true, CanRedo: true } });\n expect((enabled.nativeElement.querySelector('button[title=\"Undo\"]') as HTMLButtonElement).disabled).toBe(false);\n expect((enabled.nativeElement.querySelector('button[title=\"Redo\"]') as HTMLButtonElement).disabled).toBe(false);\n });\n\n it('emits ToolChange with the clicked tool', () => {\n const spy = vi.fn();\n const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent, { setup: (c) => c.ToolChange.subscribe(spy) });\n // the pen tool button (3rd entry, index 2)\n const penBtn = f.nativeElement.querySelector('button[title=\"Pen\"]') as HTMLButtonElement;\n penBtn.click();\n expect(spy).toHaveBeenCalledWith('pen');\n });\n\n it('emits PenColorChange when a pen swatch is clicked', () => {\n const spy = vi.fn();\n const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent, {\n inputs: { ActiveTool: 'pen' },\n setup: (c) => c.PenColorChange.subscribe(spy),\n });\n const swatch = f.nativeElement.querySelector('.pen-flyout .pen-c') as HTMLElement;\n swatch.click();\n expect(spy).toHaveBeenCalledWith(f.componentInstance.PenColors[0]);\n });\n\n it('emits BOTH TextSizeChange and StyleSelection when a text size is picked', () => {\n const sizeSpy = vi.fn();\n const styleSpy = vi.fn();\n const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent, {\n inputs: { ActiveTool: 'text' },\n setup: (c) => {\n c.TextSizeChange.subscribe(sizeSpy);\n c.StyleSelection.subscribe(styleSpy);\n },\n });\n const sizeEl = f.nativeElement.querySelector('.text-flyout .txt-s') as HTMLElement;\n sizeEl.click();\n const firstSize = f.componentInstance.TextSizes[0];\n expect(sizeSpy).toHaveBeenCalledWith(firstSize);\n expect(styleSpy).toHaveBeenCalledWith({ FontSize: firstSize });\n });\n\n it('emits Undo / Redo when those buttons are enabled and clicked', () => {\n const undoSpy = vi.fn();\n const redoSpy = vi.fn();\n const f = renderComponentFixture(RealtimeWhiteboardToolbarComponent, {\n inputs: { CanUndo: true, CanRedo: true },\n setup: (c) => {\n c.Undo.subscribe(undoSpy);\n c.Redo.subscribe(redoSpy);\n },\n });\n (f.nativeElement.querySelector('button[title=\"Undo\"]') as HTMLButtonElement).click();\n (f.nativeElement.querySelector('button[title=\"Redo\"]') as HTMLButtonElement).click();\n expect(undoSpy).toHaveBeenCalledTimes(1);\n expect(redoSpy).toHaveBeenCalledTimes(1);\n });\n});\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=whiteboard-zoom.component.dom.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"whiteboard-zoom.component.dom.test.d.ts","sourceRoot":"","sources":["../../src/lib/whiteboard-zoom.component.dom.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,58 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { renderComponentFixture } from '@memberjunction/ng-test-utils';
3
+ import { RealtimeWhiteboardZoomComponent } from './whiteboard-zoom.component';
4
+ /**
5
+ * DOM spec for <mj-realtime-whiteboard-zoom> — the zoom cluster. Covers the percentage
6
+ * binding, the minimap on-class toggle, and the single-click @Output emissions. The
7
+ * hold-to-zoom continuous path is timer-driven and exercised in the class-level suite;
8
+ * here we assert the click contract (a plain click steps once).
9
+ */
10
+ describe('RealtimeWhiteboardZoomComponent (DOM)', () => {
11
+ const buttons = (f) => Array.from(f.nativeElement.querySelectorAll('button'));
12
+ // template order: [0]=zoom out (−), [1]=zoom in (+), [2]=Fit, [3]=Minimap
13
+ const minusBtn = (f) => buttons(f)[0];
14
+ const plusBtn = (f) => buttons(f)[1];
15
+ const fitBtn = (f) => buttons(f)[2];
16
+ const mapBtn = (f) => buttons(f)[3];
17
+ it('renders the ZoomPercent with a trailing % sign', () => {
18
+ const f = renderComponentFixture(RealtimeWhiteboardZoomComponent, { inputs: { ZoomPercent: 90 } });
19
+ expect(f.nativeElement.querySelector('.zoom-pct')?.textContent?.trim()).toBe('90%');
20
+ });
21
+ it('defaults the percentage label to 100%', () => {
22
+ const f = renderComponentFixture(RealtimeWhiteboardZoomComponent);
23
+ expect(f.nativeElement.querySelector('.zoom-pct')?.textContent?.trim()).toBe('100%');
24
+ });
25
+ it('does not apply the .on class to the minimap button when MinimapOpen is false', () => {
26
+ const f = renderComponentFixture(RealtimeWhiteboardZoomComponent, { inputs: { MinimapOpen: false } });
27
+ expect(mapBtn(f).classList.contains('on')).toBe(false);
28
+ });
29
+ it('applies the .on class to the minimap button when MinimapOpen is true', () => {
30
+ const f = renderComponentFixture(RealtimeWhiteboardZoomComponent, { inputs: { MinimapOpen: true } });
31
+ expect(mapBtn(f).classList.contains('on')).toBe(true);
32
+ });
33
+ it('emits ZoomIn on a plain click of the + button', () => {
34
+ const spy = vi.fn();
35
+ const f = renderComponentFixture(RealtimeWhiteboardZoomComponent, { setup: (c) => c.ZoomIn.subscribe(spy) });
36
+ plusBtn(f).click();
37
+ expect(spy).toHaveBeenCalledTimes(1);
38
+ });
39
+ it('emits ZoomOut on a plain click of the − button', () => {
40
+ const spy = vi.fn();
41
+ const f = renderComponentFixture(RealtimeWhiteboardZoomComponent, { setup: (c) => c.ZoomOut.subscribe(spy) });
42
+ minusBtn(f).click();
43
+ expect(spy).toHaveBeenCalledTimes(1);
44
+ });
45
+ it('emits Fit when the fit-to-content button is clicked', () => {
46
+ const spy = vi.fn();
47
+ const f = renderComponentFixture(RealtimeWhiteboardZoomComponent, { setup: (c) => c.Fit.subscribe(spy) });
48
+ fitBtn(f).click();
49
+ expect(spy).toHaveBeenCalledTimes(1);
50
+ });
51
+ it('emits ToggleMinimap when the minimap button is clicked', () => {
52
+ const spy = vi.fn();
53
+ const f = renderComponentFixture(RealtimeWhiteboardZoomComponent, { setup: (c) => c.ToggleMinimap.subscribe(spy) });
54
+ mapBtn(f).click();
55
+ expect(spy).toHaveBeenCalledTimes(1);
56
+ });
57
+ });
58
+ //# sourceMappingURL=whiteboard-zoom.component.dom.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"whiteboard-zoom.component.dom.test.js","sourceRoot":"","sources":["../../src/lib/whiteboard-zoom.component.dom.test.ts"],"names":[],"mappings":"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","sourcesContent":["import { describe, it, expect, vi } from 'vitest';\nimport { renderComponentFixture } from '@memberjunction/ng-test-utils';\nimport { RealtimeWhiteboardZoomComponent } from './whiteboard-zoom.component';\n\n/**\n * DOM spec for <mj-realtime-whiteboard-zoom> — the zoom cluster. Covers the percentage\n * binding, the minimap on-class toggle, and the single-click @Output emissions. The\n * hold-to-zoom continuous path is timer-driven and exercised in the class-level suite;\n * here we assert the click contract (a plain click steps once).\n */\ndescribe('RealtimeWhiteboardZoomComponent (DOM)', () => {\n const buttons = (f: ReturnType<typeof renderComponentFixture<RealtimeWhiteboardZoomComponent>>) =>\n Array.from(f.nativeElement.querySelectorAll('button')) as HTMLButtonElement[];\n // template order: [0]=zoom out (−), [1]=zoom in (+), [2]=Fit, [3]=Minimap\n const minusBtn = (f: ReturnType<typeof renderComponentFixture<RealtimeWhiteboardZoomComponent>>) => buttons(f)[0];\n const plusBtn = (f: ReturnType<typeof renderComponentFixture<RealtimeWhiteboardZoomComponent>>) => buttons(f)[1];\n const fitBtn = (f: ReturnType<typeof renderComponentFixture<RealtimeWhiteboardZoomComponent>>) => buttons(f)[2];\n const mapBtn = (f: ReturnType<typeof renderComponentFixture<RealtimeWhiteboardZoomComponent>>) => buttons(f)[3];\n\n it('renders the ZoomPercent with a trailing % sign', () => {\n const f = renderComponentFixture(RealtimeWhiteboardZoomComponent, { inputs: { ZoomPercent: 90 } });\n expect(f.nativeElement.querySelector('.zoom-pct')?.textContent?.trim()).toBe('90%');\n });\n\n it('defaults the percentage label to 100%', () => {\n const f = renderComponentFixture(RealtimeWhiteboardZoomComponent);\n expect(f.nativeElement.querySelector('.zoom-pct')?.textContent?.trim()).toBe('100%');\n });\n\n it('does not apply the .on class to the minimap button when MinimapOpen is false', () => {\n const f = renderComponentFixture(RealtimeWhiteboardZoomComponent, { inputs: { MinimapOpen: false } });\n expect(mapBtn(f).classList.contains('on')).toBe(false);\n });\n\n it('applies the .on class to the minimap button when MinimapOpen is true', () => {\n const f = renderComponentFixture(RealtimeWhiteboardZoomComponent, { inputs: { MinimapOpen: true } });\n expect(mapBtn(f).classList.contains('on')).toBe(true);\n });\n\n it('emits ZoomIn on a plain click of the + button', () => {\n const spy = vi.fn();\n const f = renderComponentFixture(RealtimeWhiteboardZoomComponent, { setup: (c) => c.ZoomIn.subscribe(spy) });\n plusBtn(f).click();\n expect(spy).toHaveBeenCalledTimes(1);\n });\n\n it('emits ZoomOut on a plain click of the − button', () => {\n const spy = vi.fn();\n const f = renderComponentFixture(RealtimeWhiteboardZoomComponent, { setup: (c) => c.ZoomOut.subscribe(spy) });\n minusBtn(f).click();\n expect(spy).toHaveBeenCalledTimes(1);\n });\n\n it('emits Fit when the fit-to-content button is clicked', () => {\n const spy = vi.fn();\n const f = renderComponentFixture(RealtimeWhiteboardZoomComponent, { setup: (c) => c.Fit.subscribe(spy) });\n fitBtn(f).click();\n expect(spy).toHaveBeenCalledTimes(1);\n });\n\n it('emits ToggleMinimap when the minimap button is clicked', () => {\n const spy = vi.fn();\n const f = renderComponentFixture(RealtimeWhiteboardZoomComponent, { setup: (c) => c.ToggleMinimap.subscribe(spy) });\n mapBtn(f).click();\n expect(spy).toHaveBeenCalledTimes(1);\n });\n});\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@memberjunction/ng-whiteboard",
3
- "version": "5.42.0",
3
+ "version": "5.44.0",
4
4
  "description": "MemberJunction: Generic collaborative whiteboard for any Angular application — Angular-free state engine with undo/redo + change journal, agent perception deltas, a programmatic (AI-agent-ready) mutation tool API, sandboxed HTML widgets with an input bridge, markdown panels, and self-contained HTML/SVG export",
5
5
  "main": "./dist/public-api.js",
6
6
  "typings": "./dist/public-api.d.ts",
@@ -25,6 +25,7 @@
25
25
  "devDependencies": {
26
26
  "@angular/compiler": "21.1.3",
27
27
  "@angular/compiler-cli": "21.1.3",
28
+ "@memberjunction/ng-test-utils": "5.44.0",
28
29
  "vitest": "^4.0.18"
29
30
  },
30
31
  "peerDependencies": {
@@ -33,9 +34,10 @@
33
34
  "@angular/platform-browser": "21.1.3"
34
35
  },
35
36
  "dependencies": {
36
- "@memberjunction/global": "5.42.0",
37
- "@memberjunction/ng-code-editor": "5.42.0",
38
- "@memberjunction/ng-markdown": "5.42.0",
37
+ "@memberjunction/ng-ui-components": "5.44.0",
38
+ "@memberjunction/global": "5.44.0",
39
+ "@memberjunction/ng-code-editor": "5.44.0",
40
+ "@memberjunction/ng-markdown": "5.44.0",
39
41
  "rxjs": "^7.8.2",
40
42
  "tslib": "^2.8.1"
41
43
  },