@genesislcap/pbc-notify-ui 15.8.3 → 15.9.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.
Files changed (102) hide show
  1. package/README.md +39 -0
  2. package/custom-elements-manifest.config.js +5 -0
  3. package/dist/custom-elements.json +1771 -321
  4. package/dist/dts/components/components.d.ts.map +1 -1
  5. package/dist/dts/components/foundation-inbox/inbox-base/inbox-base.d.ts +2 -0
  6. package/dist/dts/components/foundation-inbox/inbox-base/inbox-base.d.ts.map +1 -1
  7. package/dist/dts/components/foundation-inbox/inbox.styles.d.ts.map +1 -1
  8. package/dist/dts/components/foundation-inbox/inbox.template.d.ts.map +1 -1
  9. package/dist/dts/components/foundation-inbox/inbox.types.d.ts +4 -1
  10. package/dist/dts/components/foundation-inbox/inbox.types.d.ts.map +1 -1
  11. package/dist/dts/components/foundation-notification-dashboard/alert-sound-attachment.d.ts +86 -0
  12. package/dist/dts/components/foundation-notification-dashboard/alert-sound-attachment.d.ts.map +1 -0
  13. package/dist/dts/components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.d.ts +39 -1
  14. package/dist/dts/components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.d.ts.map +1 -1
  15. package/dist/dts/components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.template.d.ts.map +1 -1
  16. package/dist/dts/components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.types.d.ts +8 -0
  17. package/dist/dts/components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.types.d.ts.map +1 -1
  18. package/dist/dts/components/foundation-notification-dashboard/components/rules/rule-management.d.ts.map +1 -1
  19. package/dist/dts/components/foundation-notification-dashboard/components/templates/template-dialog/template-dialog.d.ts +41 -1
  20. package/dist/dts/components/foundation-notification-dashboard/components/templates/template-dialog/template-dialog.d.ts.map +1 -1
  21. package/dist/dts/components/foundation-notification-dashboard/components/templates/template-dialog/template-dialog.template.d.ts.map +1 -1
  22. package/dist/dts/components/foundation-notification-dashboard/components/templates/template-dialog/template-dialog.types.d.ts +2 -1
  23. package/dist/dts/components/foundation-notification-dashboard/components/templates/template-dialog/template-dialog.types.d.ts.map +1 -1
  24. package/dist/dts/components/foundation-notification-dashboard/components/templates/template-management.d.ts.map +1 -1
  25. package/dist/dts/components/foundation-notification-dashboard/styles/dynamic-rule.styles.d.ts.map +1 -1
  26. package/dist/dts/components/foundation-sounds/foundation-sounds-panel.d.ts +73 -0
  27. package/dist/dts/components/foundation-sounds/foundation-sounds-panel.d.ts.map +1 -0
  28. package/dist/dts/components/foundation-sounds/foundation-sounds-panel.styles.d.ts +2 -0
  29. package/dist/dts/components/foundation-sounds/foundation-sounds-panel.styles.d.ts.map +1 -0
  30. package/dist/dts/components/foundation-sounds/foundation-sounds-panel.template.d.ts +3 -0
  31. package/dist/dts/components/foundation-sounds/foundation-sounds-panel.template.d.ts.map +1 -0
  32. package/dist/dts/components/foundation-sounds/foundation-sounds.d.ts +34 -0
  33. package/dist/dts/components/foundation-sounds/foundation-sounds.d.ts.map +1 -0
  34. package/dist/dts/components/foundation-sounds/foundation-sounds.styles.d.ts +3 -0
  35. package/dist/dts/components/foundation-sounds/foundation-sounds.styles.d.ts.map +1 -0
  36. package/dist/dts/index.d.ts +4 -0
  37. package/dist/dts/index.d.ts.map +1 -1
  38. package/dist/dts/react.d.ts +31 -11
  39. package/dist/dts/services/inbox.service.d.ts +6 -0
  40. package/dist/dts/services/inbox.service.d.ts.map +1 -1
  41. package/dist/dts/services/rule.service.d.ts +2 -1
  42. package/dist/dts/services/rule.service.d.ts.map +1 -1
  43. package/dist/dts/utils/alert-sound.d.ts +177 -0
  44. package/dist/dts/utils/alert-sound.d.ts.map +1 -0
  45. package/dist/dts/utils/alert-sound.test.d.ts +2 -0
  46. package/dist/dts/utils/alert-sound.test.d.ts.map +1 -0
  47. package/dist/esm/components/components.js +4 -0
  48. package/dist/esm/components/foundation-inbox/inbox-base/inbox-base.js +6 -0
  49. package/dist/esm/components/foundation-inbox/inbox.styles.js +15 -0
  50. package/dist/esm/components/foundation-inbox/inbox.template.js +16 -0
  51. package/dist/esm/components/foundation-inbox/inbox.types.js +1 -0
  52. package/dist/esm/components/foundation-notification-dashboard/alert-sound-attachment.js +218 -0
  53. package/dist/esm/components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.js +17 -2
  54. package/dist/esm/components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.template.js +3 -0
  55. package/dist/esm/components/foundation-notification-dashboard/components/rules/rule-management.js +5 -1
  56. package/dist/esm/components/foundation-notification-dashboard/components/templates/template-dialog/template-dialog.js +23 -2
  57. package/dist/esm/components/foundation-notification-dashboard/components/templates/template-dialog/template-dialog.template.js +3 -0
  58. package/dist/esm/components/foundation-notification-dashboard/components/templates/template-management.js +8 -1
  59. package/dist/esm/components/foundation-notification-dashboard/styles/dynamic-rule.styles.js +31 -1
  60. package/dist/esm/components/foundation-sounds/foundation-sounds-panel.js +283 -0
  61. package/dist/esm/components/foundation-sounds/foundation-sounds-panel.styles.js +200 -0
  62. package/dist/esm/components/foundation-sounds/foundation-sounds-panel.template.js +179 -0
  63. package/dist/esm/components/foundation-sounds/foundation-sounds.js +206 -0
  64. package/dist/esm/components/foundation-sounds/foundation-sounds.styles.js +7 -0
  65. package/dist/esm/index.js +3 -0
  66. package/dist/esm/services/inbox.service.js +6 -2
  67. package/dist/esm/services/rule.service.js +35 -0
  68. package/dist/esm/utils/alert-sound.js +955 -0
  69. package/dist/esm/utils/alert-sound.test.js +25 -0
  70. package/dist/esm/utils/index.js +3 -0
  71. package/dist/pbc-notify-ui.api.json +2859 -332
  72. package/dist/pbc-notify-ui.d.ts +281 -3
  73. package/dist/react.cjs +19 -5
  74. package/dist/react.mjs +16 -4
  75. package/dist/tsconfig.tsbuildinfo +1 -1
  76. package/package.json +23 -23
  77. package/src/components/components.ts +4 -0
  78. package/src/components/foundation-inbox/inbox-base/inbox-base.ts +7 -0
  79. package/src/components/foundation-inbox/inbox.styles.ts +15 -0
  80. package/src/components/foundation-inbox/inbox.template.ts +16 -0
  81. package/src/components/foundation-inbox/inbox.types.ts +3 -0
  82. package/src/components/foundation-notification-dashboard/alert-sound-attachment.ts +283 -0
  83. package/src/components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.template.ts +3 -0
  84. package/src/components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.ts +21 -2
  85. package/src/components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.types.ts +9 -0
  86. package/src/components/foundation-notification-dashboard/components/rules/rule-management.ts +5 -1
  87. package/src/components/foundation-notification-dashboard/components/templates/template-dialog/template-dialog.template.ts +3 -0
  88. package/src/components/foundation-notification-dashboard/components/templates/template-dialog/template-dialog.ts +25 -2
  89. package/src/components/foundation-notification-dashboard/components/templates/template-dialog/template-dialog.types.ts +2 -1
  90. package/src/components/foundation-notification-dashboard/components/templates/template-management.ts +8 -1
  91. package/src/components/foundation-notification-dashboard/styles/dynamic-rule.styles.ts +31 -1
  92. package/src/components/foundation-sounds/foundation-sounds-panel.styles.ts +201 -0
  93. package/src/components/foundation-sounds/foundation-sounds-panel.template.ts +188 -0
  94. package/src/components/foundation-sounds/foundation-sounds-panel.ts +280 -0
  95. package/src/components/foundation-sounds/foundation-sounds.styles.ts +8 -0
  96. package/src/components/foundation-sounds/foundation-sounds.ts +222 -0
  97. package/src/index.ts +8 -0
  98. package/src/services/inbox.service.ts +12 -2
  99. package/src/services/rule.service.ts +37 -1
  100. package/src/utils/alert-sound.test.ts +32 -0
  101. package/src/utils/alert-sound.ts +1135 -0
  102. package/src/utils/index.ts +3 -0
@@ -0,0 +1,222 @@
1
+ import { Connect } from '@genesislcap/foundation-comms';
2
+ import { attr, customElement, GenesisElement, html } from '@genesislcap/web-core';
3
+ import { Subscription } from 'rxjs';
4
+ import { FoundationInboxService, FoundationInboxServiceStore } from '../../services/inbox.service';
5
+ import { RuleService } from '../../services/rule.service';
6
+ import {
7
+ AlertSoundPlayer,
8
+ base64ToBlob,
9
+ getAlertDynamicRuleId,
10
+ getAlertHeader,
11
+ getAlertId,
12
+ getAlertMessage,
13
+ } from '../../utils/alert-sound';
14
+ import { logger } from '../../utils/logger';
15
+ import { AlertStatus } from '../foundation-inbox/inbox.types';
16
+ import { FoundationSoundsStyles } from './foundation-sounds.styles';
17
+
18
+ /**
19
+ * Caps the NEW-alert snapshot used to prime known IDs. Realtime inserts are
20
+ * still prepended by the inbox service after this ceiling.
21
+ */
22
+ const SOUNDS_MAX_ROWS = 100;
23
+
24
+ /**
25
+ * Headless listener for inbox alert inserts. Plays configured sounds via the
26
+ * shared AlertSoundPlayer. Mount once in the app layout.
27
+ *
28
+ * Configure rules in the inbox Sounds tab with {@link FoundationSoundsPanel}.
29
+ *
30
+ * @public
31
+ */
32
+ @customElement({
33
+ name: 'foundation-sounds',
34
+ template: html`
35
+ <slot></slot>
36
+ `,
37
+ styles: FoundationSoundsStyles,
38
+ })
39
+ export class FoundationSounds extends GenesisElement {
40
+ @Connect connect: Connect;
41
+ @FoundationInboxService inboxService: FoundationInboxService;
42
+ @RuleService ruleService: RuleService;
43
+
44
+ /**
45
+ * When false, new audible alerts are tracked but do not play.
46
+ */
47
+ @attr({ attribute: 'sound-enabled', mode: 'boolean' }) soundEnabled = true;
48
+
49
+ private connectionSub?: Subscription;
50
+ private readonly player = AlertSoundPlayer.shared;
51
+ private knownAlertIds = new Set<string>();
52
+ /** In-flight server sound fetches keyed by DYNAMIC_RULE_ID. */
53
+ private readonly serverSoundFetches = new Map<string, Promise<boolean>>();
54
+
55
+ async connectedCallback() {
56
+ super.connectedCallback();
57
+
58
+ await this.player.restore();
59
+
60
+ // The await above can outlive this element (mount → unmount → mount, e.g.
61
+ // via PBCElementsRenderer re-rendering its layout target). Without this
62
+ // guard we'd wire up a subscription nothing will ever tear down, and a
63
+ // later mount adds a second one — two overlapping sounds per alert.
64
+ if (!this.isConnected) {
65
+ return;
66
+ }
67
+
68
+ this.connectionSub = this.connect.isConnected$.subscribe((isConnected) => {
69
+ if (isConnected) {
70
+ this.subscribeToAlerts();
71
+ }
72
+ });
73
+ }
74
+
75
+ disconnectedCallback() {
76
+ super.disconnectedCallback();
77
+ this.inboxService.unsubscribe(this.subscriptionHandler);
78
+ this.connectionSub?.unsubscribe();
79
+ this.connectionSub = undefined;
80
+ }
81
+
82
+ private subscribeToAlerts(): void {
83
+ this.inboxService.unsubscribe(this.subscriptionHandler);
84
+ this.knownAlertIds = new Set();
85
+
86
+ this.inboxService.subscribe(
87
+ this.subscriptionHandler,
88
+ false,
89
+ `ALERT_STATUS=='${AlertStatus.NEW}'`,
90
+ SOUNDS_MAX_ROWS,
91
+ 'ALERT_ID,MESSAGE,HEADER,DYNAMIC_RULE_ID',
92
+ );
93
+ }
94
+
95
+ private subscriptionHandler = ({ store }: { store: FoundationInboxServiceStore }) => {
96
+ const items = store?.items || [];
97
+ const current: Array<{
98
+ id: string;
99
+ message?: string;
100
+ header?: string;
101
+ dynamicRuleId?: string;
102
+ }> = [];
103
+
104
+ for (const item of items) {
105
+ const id = getAlertId(item);
106
+ if (!id) {
107
+ continue;
108
+ }
109
+ current.push({
110
+ id,
111
+ message: getAlertMessage(item),
112
+ header: getAlertHeader(item),
113
+ dynamicRuleId: getAlertDynamicRuleId(item),
114
+ });
115
+ }
116
+
117
+ // SEQUENCE_ID <= 1 is (part of) the initial snapshot rather than a
118
+ // real-time update — same boundary as `notification-listener`. A
119
+ // first-callback latch instead would misfire on a rowless bookkeeping
120
+ // message (SOURCE_REF/MORE_ROWS only), or if the snapshot itself arrives
121
+ // split across more than one message.
122
+ if ((store?.sequenceId ?? 0) <= 1) {
123
+ for (const row of current) {
124
+ this.knownAlertIds.add(row.id);
125
+ }
126
+ return;
127
+ }
128
+
129
+ if (!this.soundEnabled) {
130
+ for (const row of current) {
131
+ this.knownAlertIds.add(row.id);
132
+ }
133
+ return;
134
+ }
135
+
136
+ // Collect new rows first so known IDs stay in sync even while async
137
+ // server-sound fetches run. Play at most once per batch: user Sounds-tab
138
+ // match first, else server sound on DYNAMIC_RULE_ID — only latch after a
139
+ // successful play so a failed/in-flight fetch can fall through to later rows.
140
+ const newRows: typeof current = [];
141
+ for (const row of current) {
142
+ if (this.knownAlertIds.has(row.id)) {
143
+ continue;
144
+ }
145
+ this.knownAlertIds.add(row.id);
146
+ newRows.push(row);
147
+ }
148
+ if (newRows.length) {
149
+ void this.playForNewAlerts(newRows);
150
+ }
151
+ };
152
+
153
+ private async playForNewAlerts(
154
+ rows: Array<{
155
+ id: string;
156
+ message?: string;
157
+ header?: string;
158
+ dynamicRuleId?: string;
159
+ }>,
160
+ ): Promise<void> {
161
+ for (const row of rows) {
162
+ if (this.player.playForAlert(row)) {
163
+ return;
164
+ }
165
+ if (row.dynamicRuleId && !this.player.isMuted) {
166
+ // Sequential: stop on first successful play; do not race later rows.
167
+ // eslint-disable-next-line no-await-in-loop -- try each alert until one sound plays
168
+ if (await this.tryPlayServerSound(row.dynamicRuleId)) {
169
+ return;
170
+ }
171
+ }
172
+ }
173
+ }
174
+
175
+ private async tryPlayServerSound(dynamicRuleId: string): Promise<boolean> {
176
+ const id = dynamicRuleId?.trim();
177
+ if (!id || this.player.isMuted) {
178
+ return false;
179
+ }
180
+ if (this.player.hasServerSound(id)) {
181
+ return this.player.playServerSound(id);
182
+ }
183
+
184
+ let fetchPromise = this.serverSoundFetches.get(id);
185
+ if (!fetchPromise) {
186
+ fetchPromise = this.fetchAndCacheServerSound(id);
187
+ this.serverSoundFetches.set(id, fetchPromise);
188
+ try {
189
+ return await fetchPromise;
190
+ } finally {
191
+ this.serverSoundFetches.delete(id);
192
+ }
193
+ }
194
+ return fetchPromise;
195
+ }
196
+
197
+ private async fetchAndCacheServerSound(dynamicRuleId: string): Promise<boolean> {
198
+ const sound = await this.ruleService.getAlertSound(dynamicRuleId);
199
+ if (!sound?.SOUND_DATA) {
200
+ // Rule may have cleared its sound since we last played — drop stale cache.
201
+ this.player.clearServerSound(dynamicRuleId);
202
+ return false;
203
+ }
204
+ const mimeType = sound.ALERT_SOUND_MIME_TYPE || 'audio/mpeg';
205
+ let blob: Blob;
206
+ try {
207
+ blob = base64ToBlob(sound.SOUND_DATA, mimeType);
208
+ } catch (error) {
209
+ // Malformed base64 must not reject playForNewAlerts — that aborts the
210
+ // rest of the batch as an unhandled rejection.
211
+ logger.error(`Invalid alert sound data for rule ${dynamicRuleId}`, error);
212
+ this.player.clearServerSound(dynamicRuleId);
213
+ return false;
214
+ }
215
+ this.player.setServerSound(dynamicRuleId, {
216
+ blob,
217
+ fileName: sound.ALERT_SOUND_FILE_NAME,
218
+ mimeType,
219
+ });
220
+ return this.player.playServerSound(dynamicRuleId);
221
+ }
222
+ }
package/src/index.ts CHANGED
@@ -1,7 +1,15 @@
1
1
  export * from './components/foundation-inbox/inbox';
2
2
  export * from './components/foundation-inbox/components/foundation-inbox-flyout/foundation-inbox-flyout';
3
3
  export * from './components/foundation-inbox/components/foundation-inbox-counter/foundation-inbox-counter';
4
+ export * from './components/foundation-sounds/foundation-sounds';
5
+ export * from './components/foundation-sounds/foundation-sounds-panel';
4
6
  export * from './components/foundation-notification-dashboard';
7
+ export { AlertSoundPlayer, MAX_SOUND_RULES, MAX_SOUND_FILE_BYTES } from './utils/alert-sound';
8
+ export type {
9
+ AlertSoundMatchMode,
10
+ AlertSoundRuleView,
11
+ AlertSoundSelectableRule,
12
+ } from './utils/alert-sound';
5
13
  export type {
6
14
  Alert,
7
15
  ParameterEditedEventDetail,
@@ -11,6 +11,12 @@ export const INBOX_PAGE_SIZE = 20;
11
11
  export interface FoundationInboxServiceStore {
12
12
  items: Array<any>;
13
13
  moreRows: boolean;
14
+ /**
15
+ * SEQUENCE_ID of the last message applied to {@link items}. `<= 1` means the
16
+ * message was (part of) the initial snapshot rather than a real-time update —
17
+ * mirrors the boundary `notification-listener` uses (`SEQUENCE_ID > 1`).
18
+ */
19
+ sequenceId: number;
14
20
  }
15
21
 
16
22
  export type SubscribeCallback = ({ store }: { store: FoundationInboxServiceStore }) => any;
@@ -51,6 +57,7 @@ class FoundationInboxServiceImpl implements FoundationInboxService {
51
57
  public store: FoundationInboxServiceStore = {
52
58
  items: [],
53
59
  moreRows: false,
60
+ sequenceId: 0,
54
61
  };
55
62
 
56
63
  private getStream(
@@ -70,6 +77,9 @@ class FoundationInboxServiceImpl implements FoundationInboxService {
70
77
  if (result.SOURCE_REF) {
71
78
  this.sourceRef = result.SOURCE_REF;
72
79
  }
80
+ if (typeof result.SEQUENCE_ID === 'number') {
81
+ this.store.sequenceId = result.SEQUENCE_ID;
82
+ }
73
83
  this.store.moreRows = Boolean(result.MORE_ROWS);
74
84
  // getMoreRows batches continue the existing order, so append them
75
85
  // rather than prepending like real-time inserts.
@@ -127,14 +137,14 @@ class FoundationInboxServiceImpl implements FoundationInboxService {
127
137
  const found = this.subscriptions.splice(index, 1);
128
138
  found.forEach((subscription) => {
129
139
  subscription.socketSubscription.unsubscribe();
130
- subscription.callback({ store: { items: [], moreRows: false } });
140
+ subscription.callback({ store: { items: [], moreRows: false, sequenceId: 0 } });
131
141
  });
132
142
  }
133
143
 
134
144
  this.stream = undefined;
135
145
  this.sourceRef = null;
136
146
  this.fetchingMore = false;
137
- this.store = { items: [], moreRows: false };
147
+ this.store = { items: [], moreRows: false, sequenceId: 0 };
138
148
  }
139
149
  }
140
150
 
@@ -1,6 +1,9 @@
1
1
  import { Connect, Message } from '@genesislcap/foundation-comms';
2
2
  import { DI } from '@genesislcap/web-core';
3
- import type { Rule } from '../components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.types';
3
+ import type {
4
+ NotifyAlertSound,
5
+ Rule,
6
+ } from '../components/foundation-notification-dashboard/components/rules/rule-dialog/rule-dialog.types';
4
7
 
5
8
  export interface RuleService {
6
9
  subscribeRule(dynamicRuleId: string, parameterDetails: any);
@@ -9,6 +12,7 @@ export interface RuleService {
9
12
  disableRule(dynamicRuleId: string);
10
13
  getRules(): Promise<any>;
11
14
  getRuleDetails(ruleId: string): Promise<Rule>;
15
+ getAlertSound(dynamicRuleId: string): Promise<NotifyAlertSound | null>;
12
16
  createRule(rule: Rule): Promise<Message>;
13
17
  updateRule(rule: Rule): Promise<Message>;
14
18
  deleteRule(ruleId: string): void;
@@ -36,6 +40,38 @@ class RuleServiceImpl implements RuleService {
36
40
  return response.REPLY[0];
37
41
  }
38
42
 
43
+ async getAlertSound(dynamicRuleId: string): Promise<NotifyAlertSound | null> {
44
+ if (!dynamicRuleId) {
45
+ return null;
46
+ }
47
+ try {
48
+ const response = await this.connect.request('REQ_NOTIFY_ALERT_SOUND', {
49
+ REQUEST: {
50
+ DYNAMIC_RULE_ID: dynamicRuleId,
51
+ },
52
+ });
53
+ const reply = response?.REPLY;
54
+ if (!Array.isArray(reply) || !reply.length) {
55
+ return null;
56
+ }
57
+ const row = reply[0] as Record<string, string>;
58
+ const soundData = row.SOUND_DATA ?? row.soundData;
59
+ if (!soundData) {
60
+ return null;
61
+ }
62
+ return {
63
+ ALERT_SOUND_ID: row.ALERT_SOUND_ID ?? row.alertSoundId,
64
+ DYNAMIC_RULE_ID: row.DYNAMIC_RULE_ID ?? row.dynamicRuleId ?? dynamicRuleId,
65
+ ALERT_SOUND_FILE_NAME: row.ALERT_SOUND_FILE_NAME ?? row.alertSoundFileName,
66
+ ALERT_SOUND_MIME_TYPE: row.ALERT_SOUND_MIME_TYPE ?? row.alertSoundMimeType,
67
+ SOUND_DATA: soundData,
68
+ };
69
+ } catch (error) {
70
+ console.error('Failed to fetch alert sound for rule', dynamicRuleId, error);
71
+ return null;
72
+ }
73
+ }
74
+
39
75
  async createRule(rule: Rule): Promise<Message> {
40
76
  return this.connect.commitEvent('EVENT_NOTIFICATION_RULE_CREATE', {
41
77
  DETAILS: rule,
@@ -0,0 +1,32 @@
1
+ import { assert, createLogicSuite } from '@genesislcap/foundation-testing';
2
+ import { textContainsWholeWord } from './alert-sound';
3
+
4
+ const suite = createLogicSuite('alert-sound textContainsWholeWord');
5
+
6
+ suite('matches a single whole word case-insensitively', () => {
7
+ assert.ok(textContainsWholeWord('Please trade now', 'trade'));
8
+ assert.ok(textContainsWholeWord('Please TRADE now', 'trade'));
9
+ assert.not.ok(textContainsWholeWord('Please trader now', 'trade'));
10
+ });
11
+
12
+ suite('matches a multi-word phrase', () => {
13
+ assert.ok(textContainsWholeWord('Risk limit breached today', 'limit breached'));
14
+ assert.not.ok(textContainsWholeWord('Risk limit breach today', 'limit breached'));
15
+ });
16
+
17
+ suite('does not match empty or whitespace-only patterns', () => {
18
+ assert.not.ok(textContainsWholeWord('anything', ''));
19
+ assert.not.ok(textContainsWholeWord('anything', ' '));
20
+ });
21
+
22
+ suite('treats short codes as whole words (g1 vs g1h)', () => {
23
+ assert.ok(textContainsWholeWord('test g1 test', 'g1'));
24
+ assert.not.ok(textContainsWholeWord('test g1h test', 'g1'));
25
+ });
26
+
27
+ suite('matches patterns that start or end with punctuation', () => {
28
+ assert.ok(textContainsWholeWord('cost $100 today', '$100'));
29
+ assert.ok(textContainsWholeWord('status: FAILED!', 'FAILED!'));
30
+ });
31
+
32
+ suite.run();