@antelopejs/dms-marketing 0.6.1 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/config.d.ts +3 -0
- package/dist/config.js +11 -0
- package/dist/config.js.map +1 -1
- package/dist/data-api/websites.d.ts +10 -4
- package/dist/data-api/websites.js +79 -3
- package/dist/data-api/websites.js.map +1 -1
- package/dist/db/models/page_snapshots.model.d.ts +2 -0
- package/dist/db/models/page_snapshots.model.js +6 -0
- package/dist/db/models/page_snapshots.model.js.map +1 -1
- package/dist/db/models/websites.model.d.ts +1 -0
- package/dist/db/models/websites.model.js +9 -0
- package/dist/db/models/websites.model.js.map +1 -1
- package/dist/db/tables/page_snapshots.table.d.ts +1 -0
- package/dist/db/tables/page_snapshots.table.js +4 -0
- package/dist/db/tables/page_snapshots.table.js.map +1 -1
- package/dist/db/tables/websites.table.d.ts +2 -0
- package/dist/db/tables/websites.table.js +4 -0
- package/dist/db/tables/websites.table.js.map +1 -1
- package/dist/frontend-module.d.ts +6 -0
- package/dist/frontend-module.js +46 -0
- package/dist/frontend-module.js.map +1 -0
- package/dist/index.js +3 -11
- package/dist/index.js.map +1 -1
- package/dist/pages/acquisition/page.d.ts +1 -0
- package/dist/pages/acquisition/page.js +9 -4
- package/dist/pages/acquisition/page.js.map +1 -1
- package/dist/pages/blocks.d.ts +11 -2
- package/dist/pages/blocks.js +43 -5
- package/dist/pages/blocks.js.map +1 -1
- package/dist/pages/funnel/page.d.ts +1 -0
- package/dist/pages/funnel/page.js +1 -0
- package/dist/pages/funnel/page.js.map +1 -1
- package/dist/pages/funnels/page.d.ts +1 -0
- package/dist/pages/funnels/page.js +4 -2
- package/dist/pages/funnels/page.js.map +1 -1
- package/dist/pages/overview/page.d.ts +1 -0
- package/dist/pages/overview/page.js +5 -6
- package/dist/pages/overview/page.js.map +1 -1
- package/dist/pages/pages/page.d.ts +1 -0
- package/dist/pages/pages/page.js +1 -0
- package/dist/pages/pages/page.js.map +1 -1
- package/dist/pages/settings/form.js +1 -0
- package/dist/pages/settings/form.js.map +1 -1
- package/dist/pages/websites/page.d.ts +2 -3
- package/dist/pages/websites/page.js +63 -5
- package/dist/pages/websites/page.js.map +1 -1
- package/dist/routes/dashboard-tracking.js +5 -1
- package/dist/routes/dashboard-tracking.js.map +1 -1
- package/dist/routes/funnels.d.ts +2 -0
- package/dist/routes/funnels.js +6 -5
- package/dist/routes/funnels.js.map +1 -1
- package/dist/routes/public-endpoints.js +1 -1
- package/dist/routes/public-endpoints.js.map +1 -1
- package/dist/routes/snapshot.d.ts +1 -1
- package/dist/routes/snapshot.js +30 -13
- package/dist/routes/snapshot.js.map +1 -1
- package/dist/routes/stats.d.ts +10 -4
- package/dist/routes/stats.js +77 -23
- package/dist/routes/stats.js.map +1 -1
- package/dist/routes/websites.d.ts +5 -1
- package/dist/routes/websites.js +31 -13
- package/dist/routes/websites.js.map +1 -1
- package/dist/services/acquisition-report.d.ts +1 -0
- package/dist/services/acquisition-report.js +1 -0
- package/dist/services/acquisition-report.js.map +1 -1
- package/dist/services/collect.js +7 -2
- package/dist/services/collect.js.map +1 -1
- package/dist/services/dashboard-tracking.d.ts +7 -14
- package/dist/services/dashboard-tracking.js +42 -26
- package/dist/services/dashboard-tracking.js.map +1 -1
- package/dist/services/funnels-table.d.ts +3 -2
- package/dist/services/funnels-table.js +9 -9
- package/dist/services/funnels-table.js.map +1 -1
- package/dist/services/heatmap.d.ts +6 -0
- package/dist/services/heatmap.js +82 -17
- package/dist/services/heatmap.js.map +1 -1
- package/dist/services/index.d.ts +1 -0
- package/dist/services/index.js +1 -0
- package/dist/services/index.js.map +1 -1
- package/dist/services/prune.js +6 -1
- package/dist/services/prune.js.map +1 -1
- package/dist/services/realtime.d.ts +3 -0
- package/dist/services/realtime.js +49 -0
- package/dist/services/realtime.js.map +1 -0
- package/dist/services/request-body.d.ts +1 -0
- package/dist/services/request-body.js +18 -0
- package/dist/services/request-body.js.map +1 -1
- package/dist/services/sessionize.d.ts +1 -0
- package/dist/services/sessionize.js +9 -0
- package/dist/services/sessionize.js.map +1 -1
- package/dist/services/snapshots.d.ts +28 -1
- package/dist/services/snapshots.js +147 -26
- package/dist/services/snapshots.js.map +1 -1
- package/dist/services/tenant-website.d.ts +11 -0
- package/dist/services/tenant-website.js +27 -5
- package/dist/services/tenant-website.js.map +1 -1
- package/dist/services/website-report.d.ts +5 -0
- package/dist/services/website-report.js +13 -1
- package/dist/services/website-report.js.map +1 -1
- package/dist/services/workspace-scope.d.ts +31 -0
- package/dist/services/workspace-scope.js +72 -0
- package/dist/services/workspace-scope.js.map +1 -0
- package/dist/types/constants.d.ts +7 -0
- package/dist/types/constants.js +8 -1
- package/dist/types/constants.js.map +1 -1
- package/dist/types/events.d.ts +25 -25
- package/dist/types/snapshots.d.ts +29 -29
- package/dist/types/snapshots.js +1 -1
- package/dist/types/snapshots.js.map +1 -1
- package/dist/utils/funnel-steps-type.d.ts +13 -13
- package/frontend-vue/app/components/ChannelsCard.vue +19 -2
- package/frontend-vue/app/components/Context.vue +50 -376
- package/frontend-vue/app/components/FunnelBuilder.vue +28 -22
- package/frontend-vue/app/components/FunnelFigure.vue +15 -2
- package/frontend-vue/app/components/FunnelReport.vue +3 -0
- package/frontend-vue/app/components/FunnelTemplates.vue +15 -13
- package/frontend-vue/app/components/InstallGuide.vue +1 -1
- package/frontend-vue/app/components/PagePreview.vue +330 -74
- package/frontend-vue/app/components/PagesExplorer.vue +230 -85
- package/frontend-vue/app/components/TopListTabs.vue +51 -18
- package/frontend-vue/app/components/WebsiteCard.vue +394 -0
- package/frontend-vue/app/components/WebsiteCreate.vue +159 -0
- package/frontend-vue/app/components/WebsitePicker.vue +210 -0
- package/frontend-vue/app/components/WebsiteSettings.vue +539 -0
- package/frontend-vue/app/components/WorkspaceScope.vue +119 -0
- package/frontend-vue/app/composables/useContextWebsite.ts +105 -0
- package/frontend-vue/app/composables/useCopyDraft.ts +47 -0
- package/frontend-vue/app/composables/useMarketingApi.ts +92 -4
- package/frontend-vue/app/composables/useMarketingReads.ts +32 -23
- package/frontend-vue/app/composables/useMarketingRoutes.ts +25 -2
- package/frontend-vue/app/composables/useRecordCrumb.ts +17 -0
- package/frontend-vue/app/composables/useSnapshotFrame.ts +35 -1
- package/frontend-vue/app/utils/anchor-drift.ts +58 -0
- package/frontend-vue/app/utils/drift-dismissals.ts +20 -0
- package/frontend-vue/app/utils/fill-height.ts +141 -0
- package/frontend-vue/app/utils/format.ts +21 -2
- package/frontend-vue/app/utils/funnel-draft.ts +22 -0
- package/frontend-vue/app/utils/heatmap.ts +17 -0
- package/frontend-vue/app/utils/page-snapshot.ts +75 -0
- package/frontend-vue/app/utils/snapshot-options.ts +148 -0
- package/frontend-vue/app/utils/snapshotScrollRoot.ts +93 -0
- package/frontend-vue/app/utils/website-card.ts +93 -0
- package/frontend-vue/app/utils/website-purge.ts +95 -0
- package/frontend-vue/app/utils/website-settings.ts +121 -0
- package/frontend-vue/app/utils/website-state.ts +93 -0
- package/frontend-vue/app/utils/workspace-scope.ts +51 -0
- package/frontend-vue/i18n/locales/marketing-en-GB.json +112 -36
- package/frontend-vue/i18n/locales/marketing-fr-FR.json +112 -36
- package/frontend-vue/package.json +1 -1
- package/frontend-vue/tests/anchor-drift.test.ts +59 -0
- package/frontend-vue/tests/build-flags.test.ts +38 -0
- package/frontend-vue/tests/context-bar.test.ts +226 -0
- package/frontend-vue/tests/copy-draft.test.ts +64 -0
- package/frontend-vue/tests/drift-dismissals.test.ts +12 -0
- package/frontend-vue/tests/fill-height.test.ts +110 -0
- package/frontend-vue/tests/format.test.ts +14 -0
- package/frontend-vue/tests/funnel-draft.test.ts +37 -0
- package/frontend-vue/tests/heatmap.test.ts +25 -0
- package/frontend-vue/tests/install-guide.test.ts +25 -0
- package/frontend-vue/tests/marketing-reads.test.ts +20 -6
- package/frontend-vue/tests/page-snapshot.test.ts +62 -0
- package/frontend-vue/tests/record-crumb.test.ts +43 -0
- package/frontend-vue/tests/registration.test.ts +4 -1
- package/frontend-vue/tests/routes.test.ts +21 -0
- package/frontend-vue/tests/save-funnel.test.ts +47 -0
- package/frontend-vue/tests/server-parity.test.ts +3 -0
- package/frontend-vue/tests/snapshot-options.test.ts +148 -0
- package/frontend-vue/tests/snapshot-scroll-root.test.ts +84 -0
- package/frontend-vue/tests/stubs/frontend-module.ts +5 -1
- package/frontend-vue/tests/top-list-tabs.test.ts +158 -0
- package/frontend-vue/tests/website-card.test.ts +119 -0
- package/frontend-vue/tests/website-purge.test.ts +70 -0
- package/frontend-vue/tests/website-settings.test.ts +133 -0
- package/frontend-vue/tests/website-state.test.ts +96 -0
- package/frontend-vue/tests/workspace-scope-control.test.ts +113 -0
- package/frontend-vue/tests/workspace-scope.test.ts +55 -0
- package/package.json +5 -4
- package/skills/dms-marketing/SKILL.md +47 -6
- package/static/snapshot.js +539 -48
- package/static/tracker.js +154 -5
- package/frontend-vue/app/components/WebsitesGrid.vue +0 -812
|
@@ -105,8 +105,8 @@
|
|
|
105
105
|
"name": "Avis de collecte en pause"
|
|
106
106
|
},
|
|
107
107
|
"context": {
|
|
108
|
-
"description": "
|
|
109
|
-
"name": "
|
|
108
|
+
"description": "Affiche les blocs de la page une fois un site choisi, ou les guides de démarrage et d'installation.",
|
|
109
|
+
"name": "Accès au site"
|
|
110
110
|
},
|
|
111
111
|
"devices": {
|
|
112
112
|
"description": "Sessions par type d'appareil.",
|
|
@@ -168,6 +168,10 @@
|
|
|
168
168
|
"description": "Les pages suivies et leurs heatmaps de clics et de défilement.",
|
|
169
169
|
"name": "Pages et heatmaps"
|
|
170
170
|
},
|
|
171
|
+
"period": {
|
|
172
|
+
"description": "La période et la comparaison que lisent tous les blocs de la page.",
|
|
173
|
+
"name": "Période"
|
|
174
|
+
},
|
|
171
175
|
"quality": {
|
|
172
176
|
"description": "Taux de rebond, durée moyenne de session et pages par session.",
|
|
173
177
|
"name": "Qualité des sessions"
|
|
@@ -196,6 +200,10 @@
|
|
|
196
200
|
"description": "La part des pages vues qui enregistrent les clics.",
|
|
197
201
|
"name": "Échantillonnage"
|
|
198
202
|
},
|
|
203
|
+
"toolbar": {
|
|
204
|
+
"description": "La barre de la page : le site, Direct / Pause et la période que lisent tous les blocs de la page.",
|
|
205
|
+
"name": "Barre site et période"
|
|
206
|
+
},
|
|
199
207
|
"tops": {
|
|
200
208
|
"description": "Contenu, acquisition, audience et événements personnalisés.",
|
|
201
209
|
"name": "Classements"
|
|
@@ -232,13 +240,17 @@
|
|
|
232
240
|
"description": "Sessions par jour, ou par heure sur une seule journée.",
|
|
233
241
|
"name": "Sessions dans le temps"
|
|
234
242
|
},
|
|
243
|
+
"website_picker": {
|
|
244
|
+
"description": "Choisit le site que lisent tous les blocs de la page, avec son état de suivi.",
|
|
245
|
+
"name": "Choix du site"
|
|
246
|
+
},
|
|
235
247
|
"website_waiting": {
|
|
236
248
|
"description": "Affichée tant que le site n'a envoyé aucune page vue, avec un lien vers le guide d'installation.",
|
|
237
249
|
"name": "Alerte première page vue"
|
|
238
250
|
},
|
|
239
|
-
"
|
|
240
|
-
"
|
|
241
|
-
"
|
|
251
|
+
"websites_table": {
|
|
252
|
+
"name": "Sites",
|
|
253
|
+
"description": "Les sites suivis en cartes, chacune ouvrant ses paramètres."
|
|
242
254
|
}
|
|
243
255
|
},
|
|
244
256
|
"builder": {
|
|
@@ -332,7 +344,7 @@
|
|
|
332
344
|
"window_max": "Max {max} h",
|
|
333
345
|
"window_presets": {
|
|
334
346
|
"1": "1 h",
|
|
335
|
-
"24": "
|
|
347
|
+
"24": "24 h"
|
|
336
348
|
},
|
|
337
349
|
"step_matched_as": "Cherché comme {path}",
|
|
338
350
|
"step_unseen": "Aucun événement brut ne lui correspond ces 30 derniers jours : les funnels comptent les événements bruts, pas les statistiques journalières ci-contre"
|
|
@@ -376,40 +388,34 @@
|
|
|
376
388
|
},
|
|
377
389
|
"context": {
|
|
378
390
|
"add_website": "Ajouter un site",
|
|
379
|
-
"comparisons": {
|
|
380
|
-
"none": "Sans comparaison",
|
|
381
|
-
"previous-period": "vs période précédente",
|
|
382
|
-
"previous-year": "vs année précédente"
|
|
383
|
-
},
|
|
384
391
|
"error": {
|
|
385
392
|
"description": "Le suivi continue et rien n'est perdu. Réessayez dans un instant.",
|
|
386
393
|
"title": "Impossible de charger vos sites"
|
|
387
394
|
},
|
|
388
395
|
"idle": "Dernier événement {ago}",
|
|
389
396
|
"imported": "Historique importé · dernier événement {ago}",
|
|
397
|
+
"just_now": "à l'instant",
|
|
390
398
|
"label": "Site et période",
|
|
391
399
|
"live": "En direct · dernier événement {ago}",
|
|
392
400
|
"no_website": "Aucun site pour l'instant",
|
|
393
|
-
"presets": {
|
|
394
|
-
"custom": "Personnalisée",
|
|
395
|
-
"last-30-days": "30 j",
|
|
396
|
-
"last-7-days": "7 j",
|
|
397
|
-
"last-90-days": "90 j",
|
|
398
|
-
"today": "Aujourd'hui"
|
|
399
|
-
},
|
|
400
401
|
"range_cut": "Seuls les {days} derniers jours de cette plage sont affichés.",
|
|
401
|
-
"refresh": "Actualiser",
|
|
402
402
|
"state": {
|
|
403
403
|
"imported": "Historique importé uniquement",
|
|
404
404
|
"live": "En direct",
|
|
405
405
|
"paused": "Suivi en pause pour ce site",
|
|
406
406
|
"waiting": "En attente du premier événement"
|
|
407
407
|
},
|
|
408
|
+
"state_short": {
|
|
409
|
+
"imported": "Importé",
|
|
410
|
+
"live": "En direct",
|
|
411
|
+
"paused": "En pause",
|
|
412
|
+
"waiting": "En attente"
|
|
413
|
+
},
|
|
408
414
|
"switch_error": "Impossible de passer sur ce site",
|
|
409
415
|
"switch_website": "Changer de site",
|
|
410
416
|
"utc_days": "Les statistiques sont comptées par jour UTC : chaque jour affiché ici commence à {time}, heure locale.",
|
|
411
|
-
"vs_previous": "vs période précédente",
|
|
412
417
|
"vs_previous_short": "vs précédente",
|
|
418
|
+
"vs_previous_year_short": "vs année précédente",
|
|
413
419
|
"waiting_guide": {
|
|
414
420
|
"action": "Ouvrir le guide d'installation",
|
|
415
421
|
"description": "Ajoutez le traceur au site, puis ouvrez une de ses pages : les premiers chiffres apparaissent ici en moins d'une minute.",
|
|
@@ -427,6 +433,7 @@
|
|
|
427
433
|
"xr": "Casque XR"
|
|
428
434
|
},
|
|
429
435
|
"errors": {
|
|
436
|
+
"dashboard_website_kept": "C'est le site du tableau de bord lui-même : il ne peut pas être supprimé. Mettez son suivi en pause ou purgez ses données à la place.",
|
|
430
437
|
"duplicate_host": "Un autre site de cet espace déclare déjà cet hôte.",
|
|
431
438
|
"duplicate_host_named": "{site} déclare déjà {host} : un hôte n'appartient qu'à un site de l'espace.",
|
|
432
439
|
"duplicate_experiment_key": "Un autre test A/B de ce site utilise déjà cette clé.",
|
|
@@ -451,7 +458,8 @@
|
|
|
451
458
|
},
|
|
452
459
|
"snapshot_retention_above_raw": "Les instantanés de page ne peuvent pas durer plus longtemps que les événements bruts : ils survivraient aux clics qu'ils illustrent.",
|
|
453
460
|
"statistics_retention_below_raw": "Les statistiques ne peuvent pas durer moins longtemps que les événements bruts : funnels et résultats A/B perdraient les sessions de leurs événements.",
|
|
454
|
-
"website_not_found": "Ce site n'existe pas ou appartient à un autre espace de travail."
|
|
461
|
+
"website_not_found": "Ce site n'existe pas ou appartient à un autre espace de travail.",
|
|
462
|
+
"workspace_not_found": "Cet espace n'existe pas, n'a pas de suivi du tableau de bord, ou vous n'avez pas accès à ses données."
|
|
455
463
|
},
|
|
456
464
|
"experiment": {
|
|
457
465
|
"colours_note": "Chaque variante garde sa couleur partout : A est neutre, B a la couleur principale. Le vert et le rouge ne signifient qu'un verdict.",
|
|
@@ -724,6 +732,7 @@
|
|
|
724
732
|
"coverage": {
|
|
725
733
|
"period_title": "Les funnels ne couvrent cette période qu'à partir du {date}",
|
|
726
734
|
"previous_title": "Pas de comparaison avec la période précédente",
|
|
735
|
+
"previous_year_title": "Pas de comparaison avec l'année précédente",
|
|
727
736
|
"description": "Les funnels sont calculés sur les événements bruts, conservés à partir du {date} : les jours antérieurs ne comptent rien, et une période de comparaison qu'ils ne couvrent pas est écartée plutôt qu'affichée comme une énorme variation."
|
|
728
737
|
}
|
|
729
738
|
},
|
|
@@ -902,7 +911,6 @@
|
|
|
902
911
|
},
|
|
903
912
|
"list_truncated": "Seules les {count} premières pages sont listées. Filtrez, ou saisissez un chemin, pour en trouver une autre.",
|
|
904
913
|
"missing_hint": "Une page manque ? Saisissez son chemin exact ci-dessus.",
|
|
905
|
-
"more_list": "Autres actions de la liste",
|
|
906
914
|
"most_clicked": "Les plus cliqués",
|
|
907
915
|
"most_clicked_empty": "Aucun élément ancré pour l'instant : les clics s'affichent seulement sur la page.",
|
|
908
916
|
"no_match": {
|
|
@@ -923,7 +931,12 @@
|
|
|
923
931
|
"all_masked": "Tout le texte est masqué",
|
|
924
932
|
"approximate_layout": "Instantané {layout} affiché à {width} px",
|
|
925
933
|
"approximate_layout_hint": "Aucun instantané n'a encore été pris à cette largeur : la mise en page est recalculée. Les clics restent sur leur élément.",
|
|
934
|
+
"captured_from": "depuis {url}",
|
|
935
|
+
"captured_from_hint": "La page sur laquelle l'instantané a été pris. Sa chaîne de requête et son fragment ne sont jamais conservés : ils peuvent contenir des jetons ou des adresses.",
|
|
936
|
+
"drift": "{share} des clics visent des éléments absents de cet instantané",
|
|
937
|
+
"drift_hint": "La page a peut-être changé depuis le {date}. Ces clics sont placés là où ils sont tombés dans la page, pas sur leur élément. Réinitialiser la page efface ses clics et son instantané pour repartir de la version actuelle.",
|
|
926
938
|
"enable_snapshots": "Activer les instantanés de page",
|
|
939
|
+
"end_of_page": "Fin de la page",
|
|
927
940
|
"fields_masked": "Texte visible, champs de formulaire masqués",
|
|
928
941
|
"layout": "Format",
|
|
929
942
|
"layout_desktop": "Ordinateur",
|
|
@@ -942,12 +955,15 @@
|
|
|
942
955
|
"overflow_hint": "L'instantané se dispose sur {width} px. La heatmap suit le document et reste donc alignée ; l'instantané, lui, peut être coupé.",
|
|
943
956
|
"oversized": "Page trop haute pour un aperçu fidèle",
|
|
944
957
|
"oversized_hint": "Le document dépasse la plus grande surface dessinable : la heatmap et l'instantané ne coïncident plus. Les chiffres de clics ne sont pas affectés.",
|
|
958
|
+
"snapshot_settings": "Réglages des instantanés",
|
|
945
959
|
"snapshots_enabled": "Instantanés de page",
|
|
946
960
|
"snapshots_off": "Les instantanés de page sont désactivés pour ce site",
|
|
947
961
|
"snapshots_off_hint": "Les clics et défilements sont toujours comptés, mais la heatmap ne se dessine que par-dessus un instantané de la page telle que les visiteurs la voient : son texte et ses images sont alors stockés, tous les champs de formulaire masqués, pendant la rétention des instantanés fixée dans les réglages du module. Activez-le si c'est compatible avec les engagements de confidentialité de ce site.",
|
|
948
962
|
"status_line": "Instantané du {date} · {layout} {width} px",
|
|
963
|
+
"strip_details": "Pourquoi cet avertissement",
|
|
949
964
|
"too_heavy": "Page trop lourde pour un instantané",
|
|
950
|
-
"too_heavy_hint": "
|
|
965
|
+
"too_heavy_hint": "L'instantané de la dernière page vue échantillonnée pesait {size} Mio à l'envoi (compressé quand le navigateur le permet), au-delà du plafond de {limit} Mio : cette page n'a donc pas d'instantané. Les chiffres de clics et de défilement ci-dessus n'en sont pas affectés. Un nouvel essai a lieu chaque jour : un balisage plus léger — ou une section lourde marquée data-dms-marketing-block — la ramène sous le plafond, et l'option snapshotMaxBytes du module relève le plafond.",
|
|
966
|
+
"workspace_masked": "Page d'un autre espace : tout le texte est masqué"
|
|
951
967
|
},
|
|
952
968
|
"reset": {
|
|
953
969
|
"and": "{first} et {second}",
|
|
@@ -962,13 +978,16 @@
|
|
|
962
978
|
"page_description": "Supprime les clics, profondeurs de défilement et instantanés de cette page, quelle que soit la période. Saisissez son chemin pour confirmer.",
|
|
963
979
|
"page_done": "Heatmap de la page réinitialisée",
|
|
964
980
|
"page_title": "Réinitialiser la heatmap de cette page ?",
|
|
965
|
-
"site_button": "Réinitialiser toutes les heatmaps…",
|
|
966
|
-
"site_description": "Supprime tous les clics, profondeurs de défilement et instantanés de page du site, quelle que soit la période. Saisissez le domaine pour confirmer.",
|
|
967
|
-
"site_done": "Heatmaps de {name} réinitialisées",
|
|
968
|
-
"site_title": "Réinitialiser les heatmaps de {name} ?",
|
|
969
981
|
"snapshots": "{count} instantané | {count} instantanés"
|
|
970
982
|
},
|
|
971
983
|
"row_clicks": "{count} clic | {count} clics",
|
|
984
|
+
"row_snapshot": {
|
|
985
|
+
"none": "Pas d'instantané",
|
|
986
|
+
"none_title": "Pas encore d'instantané : la prochaine visite échantillonnée en prendra un",
|
|
987
|
+
"title": "Instantané du {date} · {layouts} | Instantanés jusqu'au {date} · {layouts}",
|
|
988
|
+
"too_heavy": "Trop lourde",
|
|
989
|
+
"too_heavy_title": "Page trop lourde pour un instantané (essai du {date})"
|
|
990
|
+
},
|
|
972
991
|
"row_views": "{count} vue | {count} vues",
|
|
973
992
|
"sampled_unit": "vue échantillonnée | vues échantillonnées",
|
|
974
993
|
"sampling_note": "Clics et défilements ont été enregistrés sur {rate} des pages vues sur cette période.",
|
|
@@ -1007,6 +1026,16 @@
|
|
|
1007
1026
|
"description": "Les pages et heatmaps apparaissent dès que le traceur est sur le site et qu'une première page est chargée.",
|
|
1008
1027
|
"title": "{website} n'a encore envoyé aucune page vue"
|
|
1009
1028
|
},
|
|
1029
|
+
"workspace": {
|
|
1030
|
+
"all": "Tous les espaces",
|
|
1031
|
+
"all_hint": "{count} espace combiné | {count} espaces combinés",
|
|
1032
|
+
"combined_note": "Combiné sur {count} espace, en lecture seule. Les instantanés s'affichent avec leur texte masqué. | Combiné sur {count} espaces, en lecture seule. Les instantanés s'affichent avec leur texte masqué.",
|
|
1033
|
+
"label": "Espace :",
|
|
1034
|
+
"note_label": "À propos de cette vue",
|
|
1035
|
+
"other_note": "Tableau de bord d'un autre espace, en lecture seule. Les instantanés s'affichent avec leur texte masqué.",
|
|
1036
|
+
"switch": "Changer l'espace dont les heatmaps sont lues",
|
|
1037
|
+
"yours": "Votre espace"
|
|
1038
|
+
},
|
|
1010
1039
|
"quoted": "« {text} »"
|
|
1011
1040
|
},
|
|
1012
1041
|
"quick_actions": {
|
|
@@ -1069,13 +1098,17 @@
|
|
|
1069
1098
|
},
|
|
1070
1099
|
"column": {
|
|
1071
1100
|
"domain": "Domaine",
|
|
1072
|
-
"name": "Nom"
|
|
1101
|
+
"name": "Nom",
|
|
1102
|
+
"tracking": "Suivi",
|
|
1103
|
+
"snapshots": "Instantanés de page",
|
|
1104
|
+
"created": "Ajouté"
|
|
1073
1105
|
},
|
|
1074
1106
|
"create": "Ajouter un site",
|
|
1075
1107
|
"delete": {
|
|
1076
1108
|
"action": "Supprimer le site…",
|
|
1077
1109
|
"confirm": "Supprimer le site",
|
|
1078
1110
|
"description": "Tout ce qui a été suivi sur ce site disparaît, pour toutes les périodes. Saisissez le domaine pour confirmer.",
|
|
1111
|
+
"dashboard_kept": "C'est le site du tableau de bord lui-même, que le module suit tout seul : il ne peut pas être supprimé. Mettez son suivi en pause ou purgez ses données à la place.",
|
|
1079
1112
|
"done": "{name} supprimé",
|
|
1080
1113
|
"funnels": "Funnels et leurs tests A/B",
|
|
1081
1114
|
"impact_error": "Impossible de lister ce que la suppression de ce site effacerait.",
|
|
@@ -1088,14 +1121,38 @@
|
|
|
1088
1121
|
"snapshots": "Instantanés de page",
|
|
1089
1122
|
"title": "Supprimer {name} ?"
|
|
1090
1123
|
},
|
|
1124
|
+
"purge": {
|
|
1125
|
+
"all_action": "Purger toutes les données…",
|
|
1126
|
+
"all_confirm": "Purger toutes les données",
|
|
1127
|
+
"all_description": "Tout ce qui a été collecté sur ce site disparaît, pour toutes les périodes : pages vues, sessions, statistiques quotidiennes, clics, profondeurs de défilement, instantanés, expositions A/B et historique importé. Le site, ses réglages et ses funnels restent ; leurs résultats repartent de zéro. Saisissez le domaine pour confirmer.",
|
|
1128
|
+
"all_done": "Données de {name} purgées",
|
|
1129
|
+
"all_hint": "Tout ce qui a été collecté : trafic, sessions, heatmaps, expositions A/B et historique importé.",
|
|
1130
|
+
"all_title": "Purger toutes les données de {name} ?",
|
|
1131
|
+
"done_all_counts": "Supprimé : {events} événements, {sessions} sessions et {snapshots} instantanés.",
|
|
1132
|
+
"done_heatmaps_counts": "Supprimé : {events} clics et profondeurs de défilement, et {snapshots} instantanés.",
|
|
1133
|
+
"heatmap_events": "Clics et profondeurs de défilement, depuis le début",
|
|
1134
|
+
"heatmaps_action": "Purger les heatmaps…",
|
|
1135
|
+
"heatmaps_confirm": "Purger les heatmaps",
|
|
1136
|
+
"heatmaps_description": "Les clics, profondeurs de défilement et instantanés de toutes les pages disparaissent, pour toutes les périodes : les nombres de clics retombent à zéro partout. Pages vues, sessions et funnels sont conservés. Saisissez le domaine pour confirmer.",
|
|
1137
|
+
"heatmaps_done": "Heatmaps de {name} purgées",
|
|
1138
|
+
"heatmaps_hint": "Clics, profondeurs de défilement et instantanés de toutes les pages. Le trafic est conservé.",
|
|
1139
|
+
"heatmaps_title": "Purger les heatmaps de {name} ?",
|
|
1140
|
+
"kept": "Le site, ses réglages, ses funnels et ses tests A/B sont conservés",
|
|
1141
|
+
"kept_traffic": "Pages vues, sessions, funnels et tests A/B sont conservés",
|
|
1142
|
+
"section": "Purger les données collectées",
|
|
1143
|
+
"section_description": "Le site garde ses réglages, ses funnels et ses tests A/B ; leurs résultats repartent de zéro."
|
|
1144
|
+
},
|
|
1091
1145
|
"description": "Les sites qui envoient des données à cet espace, leur traceur et ce qui est enregistré sur chacun.",
|
|
1092
|
-
"
|
|
1093
|
-
"
|
|
1094
|
-
"
|
|
1095
|
-
"
|
|
1096
|
-
"
|
|
1097
|
-
"
|
|
1098
|
-
"
|
|
1146
|
+
"discard_confirm": {
|
|
1147
|
+
"title": "Supprimer les instantanés du site ?",
|
|
1148
|
+
"description": "Modifier cette option supprime l'instantané de page du site. Les clics et la profondeur de défilement sont conservés ; les prochaines visites échantillonnées recaptureront les pages. | Modifier cette option supprime les {count} instantanés de page du site. Les clics et la profondeur de défilement sont conservés ; les prochaines visites échantillonnées recaptureront les pages.",
|
|
1149
|
+
"description_unknown": "Modifier cette option supprime les instantanés de page existants du site. Les clics et la profondeur de défilement sont conservés ; les prochaines visites échantillonnées recaptureront les pages.",
|
|
1150
|
+
"confirm": "Modifier et supprimer",
|
|
1151
|
+
"cancel": "Garder les instantanés",
|
|
1152
|
+
"cancel_none": "Annuler",
|
|
1153
|
+
"confirm_none": "Modifier",
|
|
1154
|
+
"description_none": "Le site ne détient encore aucun instantané. Les prochaines visites échantillonnées prendront ses pages avec le nouveau réglage.",
|
|
1155
|
+
"title_none": "Modifier les réglages d'instantané du site ?"
|
|
1099
1156
|
},
|
|
1100
1157
|
"error": "Impossible de charger les sites",
|
|
1101
1158
|
"fields": {
|
|
@@ -1124,6 +1181,7 @@
|
|
|
1124
1181
|
"save_error": "Le site n'a pas pu être enregistré.",
|
|
1125
1182
|
"saved": "Site enregistré",
|
|
1126
1183
|
"snapshots": "Instantanés de page",
|
|
1184
|
+
"snapshots_edit": "Réglages des instantanés de page de {name}",
|
|
1127
1185
|
"snapshots_masked": "Actives · texte masqué",
|
|
1128
1186
|
"snapshots_off": "Désactivées",
|
|
1129
1187
|
"snapshots_on": "Actives · texte visible",
|
|
@@ -1144,7 +1202,25 @@
|
|
|
1144
1202
|
"edit_named": "Paramètres du site {name}",
|
|
1145
1203
|
"snippet_named": "Balise du traceur du site {name}",
|
|
1146
1204
|
"open_overview_named": "Ouvrir la vue d'ensemble du site {name}",
|
|
1147
|
-
"finish_install_named": "Terminer l'installation du site {name}"
|
|
1205
|
+
"finish_install_named": "Terminer l'installation du site {name}",
|
|
1206
|
+
"search": "Rechercher un site",
|
|
1207
|
+
"menu_named": "Actions pour {name}",
|
|
1208
|
+
"edit": "Paramètres",
|
|
1209
|
+
"no_match": "Aucun site ne correspond à ces filtres.",
|
|
1210
|
+
"count": "{count} site | {count} sites",
|
|
1211
|
+
"modal": {
|
|
1212
|
+
"title": "Paramètres du site",
|
|
1213
|
+
"general": "Général",
|
|
1214
|
+
"danger": "Zone de danger",
|
|
1215
|
+
"added": "ajouté le {date}"
|
|
1216
|
+
},
|
|
1217
|
+
"create_modal": {
|
|
1218
|
+
"title": "Ajouter un site",
|
|
1219
|
+
"description": "Nommez le site et son domaine ; le guide d'installation vous donne ensuite la balise à coller."
|
|
1220
|
+
}
|
|
1221
|
+
},
|
|
1222
|
+
"live": {
|
|
1223
|
+
"new_events": "{count} nouvel événement | {count} nouveaux événements"
|
|
1148
1224
|
}
|
|
1149
1225
|
}
|
|
1150
1226
|
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import {
|
|
3
|
+
anchorDrift,
|
|
4
|
+
DRIFT_MIN_CLICKS,
|
|
5
|
+
DRIFT_MIN_SHARE,
|
|
6
|
+
} from '../app/utils/anchor-drift'
|
|
7
|
+
|
|
8
|
+
const anchor = (selector: string, nth = 0) => ({ selector, nth })
|
|
9
|
+
const gone = new Set(['#old-cta\u00000', '.card\u00002'])
|
|
10
|
+
const isMissing = (a: { selector: string; nth: number }) =>
|
|
11
|
+
gone.has(`${a.selector}\u0000${a.nth}`)
|
|
12
|
+
|
|
13
|
+
describe('anchor drift', () => {
|
|
14
|
+
it('counts the clicks whose element the snapshot lacks, by weight', () => {
|
|
15
|
+
const drift = anchorDrift(
|
|
16
|
+
[
|
|
17
|
+
{ weight: 40, anchor: anchor('#buy') },
|
|
18
|
+
{ weight: 6, anchor: anchor('#old-cta') },
|
|
19
|
+
{ weight: 4, anchor: anchor('.card', 2) },
|
|
20
|
+
// Same selector, an nth the snapshot still has.
|
|
21
|
+
{ weight: 10, anchor: anchor('.card', 1) },
|
|
22
|
+
// A click on the document itself cannot drift.
|
|
23
|
+
{ weight: 100 },
|
|
24
|
+
],
|
|
25
|
+
isMissing,
|
|
26
|
+
)
|
|
27
|
+
expect(drift).toEqual({
|
|
28
|
+
anchored: 60,
|
|
29
|
+
missing: 10,
|
|
30
|
+
share: 10 / 60,
|
|
31
|
+
significant: true,
|
|
32
|
+
})
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it('warns from 10 % and 5 clicks, not below either', () => {
|
|
36
|
+
expect(DRIFT_MIN_SHARE).toBe(0.1)
|
|
37
|
+
expect(DRIFT_MIN_CLICKS).toBe(5)
|
|
38
|
+
const at = (missing: number, kept: number) =>
|
|
39
|
+
anchorDrift(
|
|
40
|
+
[
|
|
41
|
+
{ weight: missing, anchor: anchor('#old-cta') },
|
|
42
|
+
{ weight: kept, anchor: anchor('#buy') },
|
|
43
|
+
],
|
|
44
|
+
isMissing,
|
|
45
|
+
).significant
|
|
46
|
+
expect(at(5, 45)).toBe(true) // 10 %, 5 clicks
|
|
47
|
+
expect(at(5, 46)).toBe(false) // under 10 %
|
|
48
|
+
expect(at(4, 0)).toBe(false) // 100 %, but 4 clicks
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('reads no drift without anchored clicks', () => {
|
|
52
|
+
expect(anchorDrift([{ weight: 9 }], isMissing)).toEqual({
|
|
53
|
+
anchored: 0,
|
|
54
|
+
missing: 0,
|
|
55
|
+
share: 0,
|
|
56
|
+
significant: false,
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
})
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { join, relative } from 'node:path'
|
|
3
|
+
import { fileURLToPath } from 'node:url'
|
|
4
|
+
import { describe, expect, it } from 'vitest'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* The DMS builds the module's frontend with Vite alone, not Nuxt: Nuxt's
|
|
8
|
+
* `import.meta.client` / `import.meta.server` are undefined there, so a branch
|
|
9
|
+
* on them never runs in the browser (nor on the server). The context gate's
|
|
10
|
+
* client-navigation wait was dead code that way. Vite's `import.meta.env.SSR`
|
|
11
|
+
* is the flag both sides define. Checked on the sources themselves so a new
|
|
12
|
+
* file cannot bring the Nuxt flags back.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const APP = fileURLToPath(new URL('../app', import.meta.url))
|
|
16
|
+
const ROOT = fileURLToPath(new URL('..', import.meta.url))
|
|
17
|
+
const NUXT_FLAG = /import\.meta\.(client|server|browser|dev|prerender)\b/
|
|
18
|
+
|
|
19
|
+
function sources(dir: string): string[] {
|
|
20
|
+
return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => {
|
|
21
|
+
const path = join(dir, entry.name)
|
|
22
|
+
if (entry.isDirectory()) {
|
|
23
|
+
return sources(path)
|
|
24
|
+
}
|
|
25
|
+
return /\.(vue|ts)$/.test(entry.name) ? [path] : []
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
describe('build flags', () => {
|
|
30
|
+
it('reads no Nuxt-only import.meta flag', () => {
|
|
31
|
+
const files = [...sources(APP), join(ROOT, 'dms.frontend.ts')]
|
|
32
|
+
const offending = files
|
|
33
|
+
.filter((file) => NUXT_FLAG.test(readFileSync(file, 'utf8')))
|
|
34
|
+
.map((file) => relative(ROOT, file).replaceAll('\\', '/'))
|
|
35
|
+
expect(files.length).toBeGreaterThan(0)
|
|
36
|
+
expect(offending).toEqual([])
|
|
37
|
+
})
|
|
38
|
+
})
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import {
|
|
2
|
+
computed,
|
|
3
|
+
createSSRApp,
|
|
4
|
+
defineComponent,
|
|
5
|
+
h,
|
|
6
|
+
nextTick,
|
|
7
|
+
ref,
|
|
8
|
+
type Ref,
|
|
9
|
+
} from 'vue'
|
|
10
|
+
import { renderToString } from 'vue/server-renderer'
|
|
11
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
12
|
+
import Context from '../app/components/Context.vue'
|
|
13
|
+
import WebsitePicker from '../app/components/WebsitePicker.vue'
|
|
14
|
+
import type { MarketingContextPayload } from '../app/composables/useMarketingContext'
|
|
15
|
+
|
|
16
|
+
const route = vi.hoisted(() => ({ query: {} as Record<string, string> }))
|
|
17
|
+
|
|
18
|
+
vi.mock('#dms/frontend-module', async (importOriginal) => ({
|
|
19
|
+
...(await importOriginal<object>()),
|
|
20
|
+
useDmsRoute: () => ({ query: route.query, path: '/' }),
|
|
21
|
+
useDmsRouter: () => ({ push: async () => {}, replace: async () => {} }),
|
|
22
|
+
}))
|
|
23
|
+
|
|
24
|
+
const NOW = Date.UTC(2026, 9, 16, 12)
|
|
25
|
+
|
|
26
|
+
const CONTEXT: MarketingContextPayload = {
|
|
27
|
+
websites: [
|
|
28
|
+
{
|
|
29
|
+
id: 'acme-shop',
|
|
30
|
+
name: 'Acme Shop',
|
|
31
|
+
domain: 'shop.example.com',
|
|
32
|
+
state: 'live',
|
|
33
|
+
lastActivityAt: NOW - 60_000,
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
selectedId: 'acme-shop',
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let contextData: Ref<MarketingContextPayload | null>
|
|
40
|
+
let readOptions: Record<string, unknown>
|
|
41
|
+
let paused: Ref<boolean>
|
|
42
|
+
let liveRefreshes: Array<{
|
|
43
|
+
refresh: () => void
|
|
44
|
+
options: Record<string, unknown>
|
|
45
|
+
}>
|
|
46
|
+
let topicHandlers: Map<string, (event: object) => void>
|
|
47
|
+
let blockRefreshes: number
|
|
48
|
+
let contextReads: number
|
|
49
|
+
|
|
50
|
+
beforeEach(() => {
|
|
51
|
+
vi.useFakeTimers({ now: NOW })
|
|
52
|
+
contextData = ref(CONTEXT)
|
|
53
|
+
readOptions = {}
|
|
54
|
+
paused = ref(false)
|
|
55
|
+
liveRefreshes = []
|
|
56
|
+
topicHandlers = new Map()
|
|
57
|
+
blockRefreshes = 0
|
|
58
|
+
contextReads = 0
|
|
59
|
+
route.query = {}
|
|
60
|
+
vi.stubGlobal(
|
|
61
|
+
'useApiData',
|
|
62
|
+
(_url: unknown, options: Record<string, unknown> = {}) => {
|
|
63
|
+
readOptions = options
|
|
64
|
+
return {
|
|
65
|
+
data: contextData,
|
|
66
|
+
error: ref(null),
|
|
67
|
+
status: ref('success'),
|
|
68
|
+
refresh: async () => {
|
|
69
|
+
contextReads += 1
|
|
70
|
+
},
|
|
71
|
+
then: (resolve: (value: undefined) => void) => resolve(undefined),
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
)
|
|
75
|
+
vi.stubGlobal('useAuthFetch', () => ({ $authFetch: async () => ({}) }))
|
|
76
|
+
vi.stubGlobal('useToast', () => ({ add: () => {} }))
|
|
77
|
+
vi.stubGlobal('useDmsState', (_key: string, init: () => unknown) =>
|
|
78
|
+
ref(init()),
|
|
79
|
+
)
|
|
80
|
+
vi.stubGlobal('usePeriodScope', () =>
|
|
81
|
+
ref({
|
|
82
|
+
range: {
|
|
83
|
+
from: new Date(NOW - 30 * 86_400_000),
|
|
84
|
+
to: new Date(NOW + 3_600_000),
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
87
|
+
)
|
|
88
|
+
vi.stubGlobal('usePageLive', () => ({
|
|
89
|
+
isPaused: computed(() => paused.value),
|
|
90
|
+
}))
|
|
91
|
+
vi.stubGlobal('refreshLivePageBlocks', () => {
|
|
92
|
+
blockRefreshes += 1
|
|
93
|
+
})
|
|
94
|
+
vi.stubGlobal(
|
|
95
|
+
'usePageLiveRefresh',
|
|
96
|
+
(refresh: () => void, options: Record<string, unknown> = {}) => {
|
|
97
|
+
liveRefreshes.push({ refresh, options })
|
|
98
|
+
return {
|
|
99
|
+
missed: computed(() => 0),
|
|
100
|
+
trigger: () => {},
|
|
101
|
+
refreshNow: refresh,
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
)
|
|
105
|
+
vi.stubGlobal(
|
|
106
|
+
'useRealtimeTopic',
|
|
107
|
+
(topic: string, handler: (event: object) => void) => {
|
|
108
|
+
topicHandlers.set(topic, handler)
|
|
109
|
+
},
|
|
110
|
+
)
|
|
111
|
+
vi.stubGlobal('readerTimeZone', () => 'UTC')
|
|
112
|
+
vi.stubGlobal('usePageToolbarSize', () => ({ size: computed(() => 'sm') }))
|
|
113
|
+
vi.stubGlobal('withQuery', (path: string) => path)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
afterEach(() => {
|
|
117
|
+
vi.useRealTimers()
|
|
118
|
+
vi.unstubAllGlobals()
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
const slot = (name: string) =>
|
|
122
|
+
defineComponent({
|
|
123
|
+
name,
|
|
124
|
+
inheritAttrs: false,
|
|
125
|
+
setup:
|
|
126
|
+
(_props, { slots }) =>
|
|
127
|
+
() =>
|
|
128
|
+
h('div', slots.default?.()),
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
const TOPIC = 'marketing.events'
|
|
132
|
+
|
|
133
|
+
async function render(
|
|
134
|
+
component: object,
|
|
135
|
+
props: Record<string, unknown> = {},
|
|
136
|
+
): Promise<string> {
|
|
137
|
+
const app = createSSRApp(
|
|
138
|
+
defineComponent({
|
|
139
|
+
setup: () => () =>
|
|
140
|
+
h(component, props, { default: () => h('main', 'blocks') }),
|
|
141
|
+
}),
|
|
142
|
+
)
|
|
143
|
+
for (const name of [
|
|
144
|
+
'UDropdownMenu',
|
|
145
|
+
'UButton',
|
|
146
|
+
'USkeleton',
|
|
147
|
+
'UIcon',
|
|
148
|
+
'UTooltip',
|
|
149
|
+
'DmsEmptyState',
|
|
150
|
+
'DmsMarketingFirstRun',
|
|
151
|
+
]) {
|
|
152
|
+
app.component(name, slot(name))
|
|
153
|
+
}
|
|
154
|
+
return renderToString(app)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const renderPicker = (props: Record<string, unknown> = {}) =>
|
|
158
|
+
render(WebsitePicker, { realtimeTopic: TOPIC, ...props })
|
|
159
|
+
|
|
160
|
+
describe('the website picker of the page toolbar', () => {
|
|
161
|
+
it("shows the website's state as a dot and the age of its last event", async () => {
|
|
162
|
+
const html = await renderPicker()
|
|
163
|
+
expect(html).toContain('Acme Shop')
|
|
164
|
+
// The full line is the tooltip and the accessible name.
|
|
165
|
+
expect(html).toContain('aria-label="page.marketing.context.live"')
|
|
166
|
+
expect(html).toMatch(/>1 min\.? ago</)
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
it("says when the reader's days start in the state's name, not in a chip", async () => {
|
|
170
|
+
vi.stubGlobal('readerTimeZone', () => 'Europe/Brussels')
|
|
171
|
+
const html = await renderPicker()
|
|
172
|
+
expect(html).toContain(
|
|
173
|
+
'aria-label="page.marketing.context.live. page.marketing.context.utc_days"',
|
|
174
|
+
)
|
|
175
|
+
expect(html).not.toMatch(/>\s*UTC\s*</)
|
|
176
|
+
})
|
|
177
|
+
|
|
178
|
+
it('names no website before there is one', async () => {
|
|
179
|
+
contextData.value = { websites: [], selectedId: null }
|
|
180
|
+
const html = await renderPicker()
|
|
181
|
+
expect(html).toContain('page.marketing.context.no_website')
|
|
182
|
+
expect(html).not.toContain('data-website-picker')
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
it("reads the websites' last events through the page's live gate, on no clock", async () => {
|
|
186
|
+
await renderPicker()
|
|
187
|
+
expect(readOptions.refreshOnPageRefresh).toBe(false)
|
|
188
|
+
expect(readOptions).not.toHaveProperty('refreshInterval')
|
|
189
|
+
const [contextLive] = liveRefreshes
|
|
190
|
+
expect(liveRefreshes).toHaveLength(1)
|
|
191
|
+
expect(contextLive?.options).toEqual({ realtimeTopic: TOPIC })
|
|
192
|
+
contextLive?.refresh()
|
|
193
|
+
expect(contextReads).toBe(1)
|
|
194
|
+
// Nothing of its own ticks the blocks.
|
|
195
|
+
await vi.advanceTimersByTimeAsync(5 * 60_000)
|
|
196
|
+
expect(blockRefreshes).toBe(0)
|
|
197
|
+
expect(topicHandlers.size).toBe(0)
|
|
198
|
+
})
|
|
199
|
+
})
|
|
200
|
+
|
|
201
|
+
describe('the context gate', () => {
|
|
202
|
+
it('draws no toolbar of its own: the page declares it', async () => {
|
|
203
|
+
route.query = { website: 'acme-shop' }
|
|
204
|
+
const html = await render(Context)
|
|
205
|
+
expect(html).toContain('blocks')
|
|
206
|
+
expect(html).not.toContain('Acme Shop')
|
|
207
|
+
expect(liveRefreshes).toHaveLength(0)
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
it('sits 16px under the page toolbar, not 24px as the page stack', async () => {
|
|
211
|
+
route.query = { website: 'acme-shop' }
|
|
212
|
+
const html = await render(Context)
|
|
213
|
+
expect(html).toMatch(/^<div class="-mt-2 flex flex-col gap-4"/)
|
|
214
|
+
})
|
|
215
|
+
|
|
216
|
+
it('holds the blocks until the URL names the website', async () => {
|
|
217
|
+
const html = await render(Context)
|
|
218
|
+
expect(html).not.toContain('blocks')
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('shows the first-run hero without a website', async () => {
|
|
222
|
+
contextData.value = { websites: [], selectedId: null }
|
|
223
|
+
const html = await render(Context)
|
|
224
|
+
expect(html).not.toContain('blocks')
|
|
225
|
+
})
|
|
226
|
+
})
|