@nyaruka/temba-components 0.132.0 → 0.134.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 (181) hide show
  1. package/CHANGELOG.md +31 -1
  2. package/demo/components/flow/example.html +1 -0
  3. package/demo/components/webchat/example.html +1 -1
  4. package/demo/static/css/tailwind.css +30019 -0
  5. package/dist/locales/es.js +5 -5
  6. package/dist/locales/es.js.map +1 -1
  7. package/dist/locales/fr.js +5 -5
  8. package/dist/locales/fr.js.map +1 -1
  9. package/dist/locales/locale-codes.js +2 -11
  10. package/dist/locales/locale-codes.js.map +1 -1
  11. package/dist/locales/pt.js +5 -5
  12. package/dist/locales/pt.js.map +1 -1
  13. package/dist/temba-components.js +555 -476
  14. package/dist/temba-components.js.map +1 -1
  15. package/out-tsc/src/display/Chat.js +248 -95
  16. package/out-tsc/src/display/Chat.js.map +1 -1
  17. package/out-tsc/src/display/FloatingTab.js +4 -4
  18. package/out-tsc/src/display/FloatingTab.js.map +1 -1
  19. package/out-tsc/src/display/TembaUser.js +3 -3
  20. package/out-tsc/src/display/TembaUser.js.map +1 -1
  21. package/out-tsc/src/events.js.map +1 -1
  22. package/out-tsc/src/flow/CanvasNode.js +132 -58
  23. package/out-tsc/src/flow/CanvasNode.js.map +1 -1
  24. package/out-tsc/src/flow/Editor.js +183 -58
  25. package/out-tsc/src/flow/Editor.js.map +1 -1
  26. package/out-tsc/src/flow/utils.js +141 -0
  27. package/out-tsc/src/flow/utils.js.map +1 -1
  28. package/out-tsc/src/interfaces.js.map +1 -1
  29. package/out-tsc/src/layout/FloatingWindow.js +1 -2
  30. package/out-tsc/src/layout/FloatingWindow.js.map +1 -1
  31. package/out-tsc/src/list/ContentMenu.js +1 -0
  32. package/out-tsc/src/list/ContentMenu.js.map +1 -1
  33. package/out-tsc/src/list/SortableList.js +3 -2
  34. package/out-tsc/src/list/SortableList.js.map +1 -1
  35. package/out-tsc/src/live/ContactChat.js +184 -205
  36. package/out-tsc/src/live/ContactChat.js.map +1 -1
  37. package/out-tsc/src/locales/es.js +5 -5
  38. package/out-tsc/src/locales/es.js.map +1 -1
  39. package/out-tsc/src/locales/fr.js +5 -5
  40. package/out-tsc/src/locales/fr.js.map +1 -1
  41. package/out-tsc/src/locales/locale-codes.js +2 -11
  42. package/out-tsc/src/locales/locale-codes.js.map +1 -1
  43. package/out-tsc/src/locales/pt.js +5 -5
  44. package/out-tsc/src/locales/pt.js.map +1 -1
  45. package/out-tsc/src/store/AppState.js +34 -0
  46. package/out-tsc/src/store/AppState.js.map +1 -1
  47. package/out-tsc/src/store/Store.js +5 -5
  48. package/out-tsc/src/store/Store.js.map +1 -1
  49. package/out-tsc/src/utils.js +3 -3
  50. package/out-tsc/src/utils.js.map +1 -1
  51. package/out-tsc/src/webchat/WebChat.js +22 -9
  52. package/out-tsc/src/webchat/WebChat.js.map +1 -1
  53. package/out-tsc/test/ActionHelper.js +6 -5
  54. package/out-tsc/test/ActionHelper.js.map +1 -1
  55. package/out-tsc/test/actions/send_broadcast.test.js +9 -4
  56. package/out-tsc/test/actions/send_broadcast.test.js.map +1 -1
  57. package/out-tsc/test/temba-contact-chat.test.js +1 -1
  58. package/out-tsc/test/temba-contact-chat.test.js.map +1 -1
  59. package/out-tsc/test/temba-floating-window.test.js +0 -2
  60. package/out-tsc/test/temba-floating-window.test.js.map +1 -1
  61. package/out-tsc/test/temba-flow-collision.test.js +673 -0
  62. package/out-tsc/test/temba-flow-collision.test.js.map +1 -0
  63. package/out-tsc/test/temba-flow-editor-node.test.js +195 -0
  64. package/out-tsc/test/temba-flow-editor-node.test.js.map +1 -1
  65. package/out-tsc/test/temba-utils-uuid.test.js +45 -1
  66. package/out-tsc/test/temba-utils-uuid.test.js.map +1 -1
  67. package/out-tsc/test/utils.test.js +2 -2
  68. package/out-tsc/test/utils.test.js.map +1 -1
  69. package/package.json +1 -1
  70. package/screenshots/truth/actions/add_contact_groups/render/descriptive-group-names.png +0 -0
  71. package/screenshots/truth/actions/add_contact_groups/render/long-group-names.png +0 -0
  72. package/screenshots/truth/actions/add_contact_groups/render/many-groups.png +0 -0
  73. package/screenshots/truth/actions/add_contact_groups/render/multiple-groups.png +0 -0
  74. package/screenshots/truth/actions/add_contact_groups/render/single-group.png +0 -0
  75. package/screenshots/truth/actions/add_contact_urn/render/expression-facebook.png +0 -0
  76. package/screenshots/truth/actions/add_contact_urn/render/expression-phone.png +0 -0
  77. package/screenshots/truth/actions/add_contact_urn/render/facebook-id.png +0 -0
  78. package/screenshots/truth/actions/add_contact_urn/render/instagram-handle.png +0 -0
  79. package/screenshots/truth/actions/add_contact_urn/render/line-id.png +0 -0
  80. package/screenshots/truth/actions/add_contact_urn/render/phone-number.png +0 -0
  81. package/screenshots/truth/actions/add_contact_urn/render/telegram-id.png +0 -0
  82. package/screenshots/truth/actions/add_contact_urn/render/viber-id.png +0 -0
  83. package/screenshots/truth/actions/add_contact_urn/render/wechat-id.png +0 -0
  84. package/screenshots/truth/actions/add_contact_urn/render/whatsapp.png +0 -0
  85. package/screenshots/truth/actions/remove_contact_groups/render/cleanup-groups.png +0 -0
  86. package/screenshots/truth/actions/remove_contact_groups/render/long-descriptive-group-names.png +0 -0
  87. package/screenshots/truth/actions/remove_contact_groups/render/many-groups.png +0 -0
  88. package/screenshots/truth/actions/remove_contact_groups/render/multiple-groups.png +0 -0
  89. package/screenshots/truth/actions/remove_contact_groups/render/remove-from-all-groups.png +0 -0
  90. package/screenshots/truth/actions/remove_contact_groups/render/single-group.png +0 -0
  91. package/screenshots/truth/actions/send_broadcast/render/contacts-only.png +0 -0
  92. package/screenshots/truth/actions/send_broadcast/render/groups-and-contacts.png +0 -0
  93. package/screenshots/truth/actions/send_broadcast/render/groups-only.png +0 -0
  94. package/screenshots/truth/actions/send_broadcast/render/many-groups.png +0 -0
  95. package/screenshots/truth/actions/send_broadcast/render/multiline-text.png +0 -0
  96. package/screenshots/truth/actions/send_broadcast/render/with-attachments.png +0 -0
  97. package/screenshots/truth/actions/send_email/render/complex-business-email.png +0 -0
  98. package/screenshots/truth/actions/send_email/render/empty-body.png +0 -0
  99. package/screenshots/truth/actions/send_email/render/empty-subject.png +0 -0
  100. package/screenshots/truth/actions/send_email/render/long-subject.png +0 -0
  101. package/screenshots/truth/actions/send_email/render/multiline-body.png +0 -0
  102. package/screenshots/truth/actions/send_email/render/multiple-recipients.png +0 -0
  103. package/screenshots/truth/actions/send_email/render/simple-email.png +0 -0
  104. package/screenshots/truth/actions/send_email/render/with-expressions.png +0 -0
  105. package/screenshots/truth/actions/send_msg/render/long-quick-replies.png +0 -0
  106. package/screenshots/truth/actions/send_msg/render/multiline-text-with-replies.png +0 -0
  107. package/screenshots/truth/actions/send_msg/render/simple-text.png +0 -0
  108. package/screenshots/truth/actions/send_msg/render/text-with-linebreaks.png +0 -0
  109. package/screenshots/truth/actions/send_msg/render/text-with-many-quick-replies.png +0 -0
  110. package/screenshots/truth/actions/send_msg/render/text-with-quick-replies.png +0 -0
  111. package/screenshots/truth/actions/send_msg/render/text-without-quick-replies.png +0 -0
  112. package/screenshots/truth/actions/start_session/render/contact-query.png +0 -0
  113. package/screenshots/truth/actions/start_session/render/contacts-only.png +0 -0
  114. package/screenshots/truth/actions/start_session/render/create-contact.png +0 -0
  115. package/screenshots/truth/actions/start_session/render/groups-and-contacts.png +0 -0
  116. package/screenshots/truth/actions/start_session/render/groups-only.png +0 -0
  117. package/screenshots/truth/actions/start_session/render/many-recipients.png +0 -0
  118. package/screenshots/truth/contacts/chat-failure.png +0 -0
  119. package/screenshots/truth/contacts/chat-for-archived-contact.png +0 -0
  120. package/screenshots/truth/contacts/chat-for-blocked-contact.png +0 -0
  121. package/screenshots/truth/contacts/chat-for-stopped-contact.png +0 -0
  122. package/screenshots/truth/contacts/chat-sends-attachments-only.png +0 -0
  123. package/screenshots/truth/contacts/chat-sends-text-and-attachments.png +0 -0
  124. package/screenshots/truth/contacts/chat-sends-text-only.png +0 -0
  125. package/screenshots/truth/floating-tab/default.png +0 -0
  126. package/screenshots/truth/floating-tab/gray.png +0 -0
  127. package/screenshots/truth/floating-tab/green.png +0 -0
  128. package/screenshots/truth/floating-tab/hover.png +0 -0
  129. package/screenshots/truth/floating-tab/purple.png +0 -0
  130. package/screenshots/truth/nodes/split_by_llm/render/information-extraction.png +0 -0
  131. package/screenshots/truth/nodes/split_by_llm/render/sentiment-analysis.png +0 -0
  132. package/screenshots/truth/nodes/split_by_llm/render/summarization.png +0 -0
  133. package/screenshots/truth/nodes/split_by_llm/render/translation-task.png +0 -0
  134. package/screenshots/truth/nodes/split_by_llm_categorize/editor/feedback-categorization.png +0 -0
  135. package/screenshots/truth/nodes/split_by_llm_categorize/render/basic-categorization.png +0 -0
  136. package/screenshots/truth/nodes/split_by_llm_categorize/render/custom-input-and-result-name.png +0 -0
  137. package/screenshots/truth/nodes/split_by_llm_categorize/render/feedback-categorization.png +0 -0
  138. package/screenshots/truth/nodes/split_by_llm_categorize/render/many-categories.png +0 -0
  139. package/screenshots/truth/nodes/split_by_llm_categorize/render/minimal-categories.png +0 -0
  140. package/screenshots/truth/nodes/split_by_random/render/ab-test-multiple-variants.png +0 -0
  141. package/screenshots/truth/nodes/split_by_random/render/sampling-split.png +0 -0
  142. package/screenshots/truth/nodes/split_by_random/render/three-way-split.png +0 -0
  143. package/screenshots/truth/nodes/split_by_random/render/two-bucket-split.png +0 -0
  144. package/screenshots/truth/nodes/wait_for_digits/render/basic-digits-wait.png +0 -0
  145. package/screenshots/truth/nodes/wait_for_digits/render/phone-number-collection.png +0 -0
  146. package/screenshots/truth/nodes/wait_for_digits/render/single-digit-with-timeout.png +0 -0
  147. package/screenshots/truth/nodes/wait_for_digits/render/verification-code.png +0 -0
  148. package/screenshots/truth/nodes/wait_for_response/render/basic-wait.png +0 -0
  149. package/screenshots/truth/nodes/wait_for_response/render/custom-result-name.png +0 -0
  150. package/screenshots/truth/nodes/wait_for_response/render/no-timeout.png +0 -0
  151. package/screenshots/truth/nodes/wait_for_response/render/short-timeout.png +0 -0
  152. package/src/display/Chat.ts +331 -135
  153. package/src/display/FloatingTab.ts +4 -4
  154. package/src/display/TembaUser.ts +3 -2
  155. package/src/events.ts +12 -12
  156. package/src/flow/CanvasNode.ts +140 -57
  157. package/src/flow/Editor.ts +240 -58
  158. package/src/flow/utils.ts +207 -1
  159. package/src/interfaces.ts +7 -0
  160. package/src/layout/FloatingWindow.ts +1 -3
  161. package/src/list/ContentMenu.ts +1 -0
  162. package/src/list/SortableList.ts +3 -2
  163. package/src/live/ContactChat.ts +195 -221
  164. package/src/locales/es.ts +13 -18
  165. package/src/locales/fr.ts +13 -18
  166. package/src/locales/locale-codes.ts +2 -11
  167. package/src/locales/pt.ts +13 -18
  168. package/src/store/AppState.ts +43 -0
  169. package/src/store/Store.ts +5 -5
  170. package/src/utils.ts +3 -3
  171. package/src/webchat/WebChat.ts +24 -10
  172. package/test/ActionHelper.ts +13 -5
  173. package/test/actions/send_broadcast.test.ts +4 -2
  174. package/test/temba-contact-chat.test.ts +1 -1
  175. package/test/temba-floating-window.test.ts +0 -2
  176. package/test/temba-flow-collision.test.ts +833 -0
  177. package/test/temba-flow-editor-node.test.ts +224 -0
  178. package/test/temba-utils-uuid.test.ts +61 -1
  179. package/test/utils.test.ts +7 -2
  180. package/test-assets/contacts/history.json +22 -9
  181. package/web-test-runner.config.mjs +3 -3
@@ -1200,6 +1200,230 @@ describe('EditorNode', () => {
1200
1200
  // 3. New JSPlumb connections are created with connectIds
1201
1201
  // This sequence ensures JSPlumb visuals stay in sync with the flow definition
1202
1202
  });
1203
+
1204
+ it('reroutes connections when removing node with multiple exits pointing to same destination', async () => {
1205
+ // Test case: node with multiple exits, but all point to the same destination
1206
+ const mockNode: Node = {
1207
+ uuid: 'test-node',
1208
+ actions: [
1209
+ {
1210
+ type: 'send_msg',
1211
+ uuid: 'action-1',
1212
+ text: 'Hello',
1213
+ quick_replies: []
1214
+ } as any
1215
+ ],
1216
+ exits: [
1217
+ { uuid: 'exit-1', destination_uuid: 'node-after' },
1218
+ { uuid: 'exit-2', destination_uuid: 'node-after' },
1219
+ { uuid: 'exit-3', destination_uuid: 'node-after' }
1220
+ ]
1221
+ };
1222
+
1223
+ const mockFlowDefinition = {
1224
+ language: 'en',
1225
+ localization: {},
1226
+ name: 'Test Flow',
1227
+ type: 'messaging' as const,
1228
+ uuid: 'test-uuid',
1229
+ revision: 1,
1230
+ spec_version: '14.3',
1231
+ nodes: [
1232
+ {
1233
+ uuid: 'node-before',
1234
+ actions: [],
1235
+ exits: [{ uuid: 'exit-before', destination_uuid: 'test-node' }]
1236
+ },
1237
+ mockNode,
1238
+ {
1239
+ uuid: 'node-after',
1240
+ actions: [],
1241
+ exits: []
1242
+ }
1243
+ ],
1244
+ _ui: {
1245
+ nodes: {},
1246
+ languages: []
1247
+ }
1248
+ };
1249
+
1250
+ // Set up the zustand store with our test flow
1251
+ const { zustand } = await import('../src/store/AppState');
1252
+ zustand.setState({
1253
+ flowDefinition: mockFlowDefinition as any
1254
+ });
1255
+
1256
+ // Verify initial state - node-before points to test-node
1257
+ const initialNodeBefore = zustand
1258
+ .getState()
1259
+ .flowDefinition.nodes.find((n) => n.uuid === 'node-before');
1260
+ expect(initialNodeBefore.exits[0].destination_uuid).to.equal('test-node');
1261
+
1262
+ // Call removeNodes to trigger the rerouting logic
1263
+ zustand.getState().removeNodes(['test-node']);
1264
+
1265
+ // Verify that the node was removed
1266
+ const remainingNodes = zustand.getState().flowDefinition.nodes;
1267
+ expect(remainingNodes.find((n) => n.uuid === 'test-node')).to.be
1268
+ .undefined;
1269
+
1270
+ // Verify that node-before's exit was rerouted to node-after
1271
+ const updatedNodeBefore = remainingNodes.find(
1272
+ (n) => n.uuid === 'node-before'
1273
+ );
1274
+ expect(updatedNodeBefore.exits[0].destination_uuid).to.equal(
1275
+ 'node-after'
1276
+ );
1277
+ });
1278
+
1279
+ it('does not reroute connections when node has exits with different destinations', async () => {
1280
+ // Test case: node with multiple exits pointing to different destinations
1281
+ const mockNode: Node = {
1282
+ uuid: 'test-node',
1283
+ actions: [
1284
+ {
1285
+ type: 'send_msg',
1286
+ uuid: 'action-1',
1287
+ text: 'Hello',
1288
+ quick_replies: []
1289
+ } as any
1290
+ ],
1291
+ exits: [
1292
+ { uuid: 'exit-1', destination_uuid: 'node-after-1' },
1293
+ { uuid: 'exit-2', destination_uuid: 'node-after-2' }
1294
+ ]
1295
+ };
1296
+
1297
+ const mockFlowDefinition = {
1298
+ language: 'en',
1299
+ localization: {},
1300
+ name: 'Test Flow',
1301
+ type: 'messaging' as const,
1302
+ uuid: 'test-uuid',
1303
+ revision: 1,
1304
+ spec_version: '14.3',
1305
+ nodes: [
1306
+ {
1307
+ uuid: 'node-before',
1308
+ actions: [],
1309
+ exits: [{ uuid: 'exit-before', destination_uuid: 'test-node' }]
1310
+ },
1311
+ mockNode,
1312
+ {
1313
+ uuid: 'node-after-1',
1314
+ actions: [],
1315
+ exits: []
1316
+ },
1317
+ {
1318
+ uuid: 'node-after-2',
1319
+ actions: [],
1320
+ exits: []
1321
+ }
1322
+ ],
1323
+ _ui: {
1324
+ nodes: {},
1325
+ languages: []
1326
+ }
1327
+ };
1328
+
1329
+ // Set up the zustand store with our test flow
1330
+ const { zustand } = await import('../src/store/AppState');
1331
+ zustand.setState({
1332
+ flowDefinition: mockFlowDefinition as any
1333
+ });
1334
+
1335
+ // Verify initial state
1336
+ const initialNodeBefore = zustand
1337
+ .getState()
1338
+ .flowDefinition.nodes.find((n) => n.uuid === 'node-before');
1339
+ expect(initialNodeBefore.exits[0].destination_uuid).to.equal('test-node');
1340
+
1341
+ // Call removeNodes
1342
+ zustand.getState().removeNodes(['test-node']);
1343
+
1344
+ // Verify that the node was removed
1345
+ const remainingNodes = zustand.getState().flowDefinition.nodes;
1346
+ expect(remainingNodes.find((n) => n.uuid === 'test-node')).to.be
1347
+ .undefined;
1348
+
1349
+ // Verify that incoming connection was cleared (set to null), not rerouted
1350
+ const updatedNodeBefore = remainingNodes.find(
1351
+ (n) => n.uuid === 'node-before'
1352
+ );
1353
+ expect(updatedNodeBefore.exits[0].destination_uuid).to.be.null;
1354
+ });
1355
+
1356
+ it('does not reroute connections when node has exits with null destinations', async () => {
1357
+ // Test case: node with some exits having null destinations
1358
+ const mockNode: Node = {
1359
+ uuid: 'test-node',
1360
+ actions: [
1361
+ {
1362
+ type: 'send_msg',
1363
+ uuid: 'action-1',
1364
+ text: 'Hello',
1365
+ quick_replies: []
1366
+ } as any
1367
+ ],
1368
+ exits: [
1369
+ { uuid: 'exit-1', destination_uuid: 'node-after' },
1370
+ { uuid: 'exit-2', destination_uuid: null }
1371
+ ]
1372
+ };
1373
+
1374
+ const mockFlowDefinition = {
1375
+ language: 'en',
1376
+ localization: {},
1377
+ name: 'Test Flow',
1378
+ type: 'messaging' as const,
1379
+ uuid: 'test-uuid',
1380
+ revision: 1,
1381
+ spec_version: '14.3',
1382
+ nodes: [
1383
+ {
1384
+ uuid: 'node-before',
1385
+ actions: [],
1386
+ exits: [{ uuid: 'exit-before', destination_uuid: 'test-node' }]
1387
+ },
1388
+ mockNode,
1389
+ {
1390
+ uuid: 'node-after',
1391
+ actions: [],
1392
+ exits: []
1393
+ }
1394
+ ],
1395
+ _ui: {
1396
+ nodes: {},
1397
+ languages: []
1398
+ }
1399
+ };
1400
+
1401
+ // Set up the zustand store with our test flow
1402
+ const { zustand } = await import('../src/store/AppState');
1403
+ zustand.setState({
1404
+ flowDefinition: mockFlowDefinition as any
1405
+ });
1406
+
1407
+ // Verify initial state
1408
+ const initialNodeBefore = zustand
1409
+ .getState()
1410
+ .flowDefinition.nodes.find((n) => n.uuid === 'node-before');
1411
+ expect(initialNodeBefore.exits[0].destination_uuid).to.equal('test-node');
1412
+
1413
+ // Call removeNodes
1414
+ zustand.getState().removeNodes(['test-node']);
1415
+
1416
+ // Verify that the node was removed
1417
+ const remainingNodes = zustand.getState().flowDefinition.nodes;
1418
+ expect(remainingNodes.find((n) => n.uuid === 'test-node')).to.be
1419
+ .undefined;
1420
+
1421
+ // Verify that incoming connection was cleared (set to null), not rerouted
1422
+ const updatedNodeBefore = remainingNodes.find(
1423
+ (n) => n.uuid === 'node-before'
1424
+ );
1425
+ expect(updatedNodeBefore.exits[0].destination_uuid).to.be.null;
1426
+ });
1203
1427
  });
1204
1428
 
1205
1429
  describe('add action button', () => {
@@ -1,5 +1,5 @@
1
1
  import { assert } from '@open-wc/testing';
2
- import { generateUUID } from '../src/utils';
2
+ import { generateUUID, generateUUIDv7 } from '../src/utils';
3
3
 
4
4
  describe('UUID Generation', () => {
5
5
  it('generates a valid UUID v4 format', () => {
@@ -45,4 +45,64 @@ describe('UUID Generation', () => {
45
45
  // All should be unique
46
46
  assert.equal(uuids.size, count, 'All generated UUIDs should be unique');
47
47
  });
48
+
49
+ it('generates a valid UUID v7 format', () => {
50
+ const uuid = generateUUIDv7();
51
+
52
+ // check that it's a string
53
+ assert.isString(uuid);
54
+
55
+ // check UUID v7 format: xxxxxxxx-xxxx-7xxx-yxxx-xxxxxxxxxxxx
56
+ const uuidPattern =
57
+ /^[0-9a-f]{8}-[0-9a-f]{4}-7[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
58
+ assert.match(uuid, uuidPattern, 'Should match UUID v7 format');
59
+
60
+ // check length
61
+ assert.equal(uuid.length, 36);
62
+
63
+ // check that it contains hyphens in the right places
64
+ assert.equal(uuid[8], '-');
65
+ assert.equal(uuid[13], '-');
66
+ assert.equal(uuid[18], '-');
67
+ assert.equal(uuid[23], '-');
68
+ });
69
+
70
+ it('generates unique UUIDs v7', () => {
71
+ const uuid1 = generateUUIDv7();
72
+ const uuid2 = generateUUIDv7();
73
+ const uuid3 = generateUUIDv7();
74
+
75
+ // all should be different
76
+ assert.notEqual(uuid1, uuid2);
77
+ assert.notEqual(uuid2, uuid3);
78
+ assert.notEqual(uuid1, uuid3);
79
+ });
80
+
81
+ it('generates time-ordered UUIDs v7', () => {
82
+ const uuid1 = generateUUIDv7();
83
+ // small delay to ensure different timestamp
84
+ const delayPromise = new Promise((resolve) => setTimeout(resolve, 5));
85
+ return delayPromise.then(() => {
86
+ const uuid2 = generateUUIDv7();
87
+
88
+ // uuid v7 should be sortable by timestamp
89
+ // the first uuid should come before the second when compared as strings
90
+ assert.isTrue(
91
+ uuid1 < uuid2,
92
+ 'Earlier UUID should be lexicographically smaller'
93
+ );
94
+ });
95
+ });
96
+
97
+ it('generates many unique UUIDs v7', () => {
98
+ const uuids = new Set();
99
+ const count = 1000;
100
+
101
+ for (let i = 0; i < count; i++) {
102
+ uuids.add(generateUUIDv7());
103
+ }
104
+
105
+ // all should be unique
106
+ assert.equal(uuids.size, count, 'All generated UUIDs should be unique');
107
+ });
48
108
  });
@@ -240,7 +240,11 @@ export const waitForCondition = async (
240
240
  }
241
241
  };
242
242
 
243
- export const assertScreenshot = async (filename: string, clip: Clip) => {
243
+ export const assertScreenshot = async (
244
+ filename: string,
245
+ clip: Clip,
246
+ waitForNetwork: boolean = false
247
+ ) => {
244
248
  // detect if we're running in copilot's environment and use adaptive threshold
245
249
  const isCopilotEnvironment = (window as any).isCopilotEnvironment;
246
250
  const threshold = isCopilotEnvironment ? 1.0 : 0.1;
@@ -251,7 +255,8 @@ export const assertScreenshot = async (filename: string, clip: Clip) => {
251
255
  `${filename}.png`,
252
256
  clip,
253
257
  exclude,
254
- threshold
258
+ threshold,
259
+ waitForNetwork
255
260
  );
256
261
  } catch (error) {
257
262
  if (error.message) {
@@ -1,8 +1,5 @@
1
1
  {
2
- "has_older": true,
3
- "recent_only": false,
4
- "next_before": 1617135091567814,
5
- "next_after": 1609359091567814,
2
+ "next": "01997d74-bf67-749a-8440-688a41c3b270",
6
3
  "events": [
7
4
  {
8
5
  "uuid": "01997d74-bf67-749a-8440-688a41c3b275",
@@ -54,7 +51,11 @@
54
51
  "name": "SMS Channel"
55
52
  }
56
53
  },
57
- "_status": {"created_on": "2025-09-24T20:40:28.239437+00:00", "status": "failed", "reason": "error_limit"},
54
+ "_status": {
55
+ "created_on": "2025-09-24T20:40:28.239437+00:00",
56
+ "status": "failed",
57
+ "reason": "error_limit"
58
+ },
58
59
  "_logs_url": null
59
60
  },
60
61
  {
@@ -89,7 +90,10 @@
89
90
  "name": "SMS Channel"
90
91
  }
91
92
  },
92
- "_status": {"created_on": "2025-09-23T20:40:28.239434+00:00", "status": "wired"},
93
+ "_status": {
94
+ "created_on": "2025-09-23T20:40:28.239434+00:00",
95
+ "status": "wired"
96
+ },
93
97
  "_logs_url": "/channels/channellog/read/1478/"
94
98
  },
95
99
  {
@@ -118,7 +122,10 @@
118
122
  "name": "SMS Channel"
119
123
  }
120
124
  },
121
- "_status": {"created_on": "2025-09-23T20:40:28.239434+00:00", "status": "wired"},
125
+ "_status": {
126
+ "created_on": "2025-09-23T20:40:28.239434+00:00",
127
+ "status": "wired"
128
+ },
122
129
  "_logs_url": "/channels/channellog/read/1476/"
123
130
  },
124
131
  {
@@ -147,7 +154,10 @@
147
154
  "name": "SMS Channel"
148
155
  }
149
156
  },
150
- "_status": {"created_on": "2025-09-23T20:40:28.239434+00:00", "status": "wired"},
157
+ "_status": {
158
+ "created_on": "2025-09-23T20:40:28.239434+00:00",
159
+ "status": "wired"
160
+ },
151
161
  "_logs_url": "/channels/channellog/read/1474/"
152
162
  },
153
163
  {
@@ -198,7 +208,10 @@
198
208
  "name": "SMS Channel"
199
209
  }
200
210
  },
201
- "_status": {"created_on": "2025-09-23T20:40:28.239434+00:00", "status": "wired"},
211
+ "_status": {
212
+ "created_on": "2025-09-23T20:40:28.239434+00:00",
213
+ "status": "wired"
214
+ },
202
215
  "_logs_url": "/channels/channellog/read/1472/"
203
216
  },
204
217
  {
@@ -145,15 +145,15 @@ const wireScreenshots = async (page, context, wait, replaceScreenshots) => {
145
145
 
146
146
  page.exposeFunction(
147
147
  'matchPageSnapshot',
148
- (filename, clip, excluded, threshold) => {
148
+ (filename, clip, excluded, threshold, waitForNetwork = false) => {
149
149
  return new Promise(async (resolve, reject) => {
150
150
  // const start = Date.now();
151
151
  const testFile = await getPath(TEST, filename);
152
152
  const truthFile = await getPath(TRUTH, filename);
153
153
 
154
- // Only wait for network idle if explicitly requested
154
+ // Wait for network idle - use per-call parameter or fall back to global wait flag
155
155
  try{
156
- if (wait) {
156
+ if (waitForNetwork || wait) {
157
157
  await page.waitForNetworkIdle({idleTime: 500, timeout: 2000});
158
158
  } else {
159
159
  await page.waitForNetworkIdle({ idleTime: 100, timeout: 1000 });