@dnax/ui 0.0.64 → 0.0.65

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.
@@ -9,6 +9,8 @@ import { icons } from "../lib/icons"
9
9
  import { cn } from "../lib/utils"
10
10
  import { useOverlayBack } from "../lib/overlayBack"
11
11
  import { placePopover, type PopoverPlacement } from "../lib/datePicker"
12
+ import { useConfigLang } from "../lib/useComponentProps"
13
+ import { dateLabels, dateLocale } from "../lib/dateI18n"
12
14
  import QDateCalendar from "./internal/QDateCalendar.vue"
13
15
 
14
16
  interface Props {
@@ -43,7 +45,7 @@ interface Props {
43
45
  width?: string
44
46
  /** Titre du panneau (modes à voile : sheet/modal/dialog) */
45
47
  title?: string
46
- /** Raccourci « Today » sous le calendrier (tous les modes) */
48
+ /** Raccourci « aujourd'hui » sous le calendrier (tous les modes) */
47
49
  todayBtn?: boolean
48
50
  /** En-tête : libellé cliquable ouvrant le choix du mois (tous les modes) */
49
51
  monthDropdown?: boolean
@@ -75,6 +77,11 @@ const emit = defineEmits<{
75
77
  clear: []
76
78
  }>()
77
79
 
80
+ // Langue de l'app (`QConfigProvider lang`) : format d'affichage et libellés.
81
+ const configLang = useConfigLang()
82
+ const locale = computed(() => dateLocale(configLang.value))
83
+ const labels = computed(() => dateLabels(configLang.value))
84
+
78
85
  /** Champ déclencheur (tout sauf `inline`) */
79
86
  const isPanelMode = computed(() => props.mode !== "inline")
80
87
  /** Panneau ancré au champ : ni voile, ni blocage du scroll, ni retour navigateur */
@@ -98,7 +105,7 @@ const formatFn = computed(
98
105
  () =>
99
106
  props.format ??
100
107
  ((d: Date) =>
101
- new Intl.DateTimeFormat(undefined, { day: "2-digit", month: "short", year: "numeric" }).format(d)),
108
+ new Intl.DateTimeFormat(locale.value, { day: "2-digit", month: "short", year: "numeric" }).format(d)),
102
109
  )
103
110
 
104
111
  const displayText = computed(() => (props.modelValue ? formatFn.value(props.modelValue) : ""))
@@ -139,7 +146,7 @@ const closePopup = () => {
139
146
  open.value = false
140
147
  }
141
148
 
142
- const sheetTitle = computed(() => props.title || props.label || "Sélectionner une date")
149
+ const sheetTitle = computed(() => props.title || props.label || labels.value.selectDate)
143
150
 
144
151
  const sheetStyle = computed<Record<string, string> | undefined>(() =>
145
152
  props.width ? { width: props.width, maxWidth: props.width } : undefined,
@@ -263,7 +270,7 @@ onBeforeUnmount(() => {
263
270
  v-if="clearable && modelValue && !disable && !readonly"
264
271
  class="q-field__clear"
265
272
  type="button"
266
- aria-label="Effacer"
273
+ :aria-label="labels.clear"
267
274
  @click.stop="onClear"
268
275
  >
269
276
  <Icon :icon="icons.x" aria-hidden="true" />
@@ -304,7 +311,7 @@ onBeforeUnmount(() => {
304
311
  <button
305
312
  type="button"
306
313
  class="q-date-picker__sheet-close"
307
- :aria-label="`Fermer ${sheetTitle}`"
314
+ :aria-label="`${labels.close} ${sheetTitle}`"
308
315
  @click="closePopup"
309
316
  >
310
317
  <Icon :icon="icons.x" aria-hidden="true" />
@@ -183,6 +183,9 @@ const scrollArrows = (dir: -1 | 1) => {
183
183
  el.scrollBy(props.vertical ? { top: delta, behavior: "smooth" } : { left: delta, behavior: "smooth" })
184
184
  }
185
185
 
186
+ /** Débordement réel → les deux flèches existent (celle du côté bloqué est **désactivée**). */
187
+ const scrollable = computed(() => canScrollStart.value || canScrollEnd.value)
188
+
186
189
  // Recalcule la position de l'indicateur (et les flèches) après chaque changement de DOM
187
190
  const measure = async () => {
188
191
  await nextTick()
@@ -285,6 +288,10 @@ const tabsClasses = computed(() =>
285
288
  props.animated && `q-tabs--anim-${props.transition}`,
286
289
  props.collapseInactive && "q-tabs--collapse-inactive",
287
290
  props.showArrows && "q-tabs--arrows",
291
+ // Fondu des bords : uniquement du côté où il reste du contenu à révéler (le CSS
292
+ // n'utilise qu'un masque alpha → rendu identique en thème clair et sombre).
293
+ props.showArrows && canScrollStart.value && "q-tabs--scroll-start",
294
+ props.showArrows && canScrollEnd.value && "q-tabs--scroll-end",
288
295
  ),
289
296
  )
290
297
  </script>
@@ -299,9 +306,10 @@ const tabsClasses = computed(() =>
299
306
  :aria-orientation="vertical ? 'vertical' : 'horizontal'"
300
307
  >
301
308
  <button
302
- v-if="showArrows && canScrollStart"
309
+ v-if="showArrows && scrollable"
303
310
  type="button"
304
311
  class="q-tabs__arrow q-tabs__arrow--start"
312
+ :disabled="!canScrollStart"
305
313
  :aria-label="vertical ? 'Defiler les onglets vers le haut' : 'Defiler les onglets vers la gauche'"
306
314
  @click="scrollArrows(-1)"
307
315
  >
@@ -317,9 +325,10 @@ const tabsClasses = computed(() =>
317
325
  <div class="q-tabs__indicator" :style="indicatorStyle" aria-hidden="true" />
318
326
  </div>
319
327
  <button
320
- v-if="showArrows && canScrollEnd"
328
+ v-if="showArrows && scrollable"
321
329
  type="button"
322
330
  class="q-tabs__arrow q-tabs__arrow--end"
331
+ :disabled="!canScrollEnd"
323
332
  :aria-label="vertical ? 'Defiler les onglets vers le bas' : 'Defiler les onglets vers la droite'"
324
333
  @click="scrollArrows(1)"
325
334
  >
@@ -4,6 +4,14 @@ import { computed, ref, watch } from "vue"
4
4
  import { Icon } from "@iconify/vue"
5
5
  import { icons } from "../../lib/icons"
6
6
  import { cn } from "../../lib/utils"
7
+ import { useConfigLang } from "../../lib/useComponentProps"
8
+ import {
9
+ dateLabels,
10
+ dateLocale,
11
+ monthShortNames,
12
+ monthYearLabel,
13
+ weekdayShortNames,
14
+ } from "../../lib/dateI18n"
7
15
 
8
16
  interface Props {
9
17
  modelValue?: Date | null
@@ -15,7 +23,7 @@ interface Props {
15
23
  disabledDates?: (d: Date) => boolean
16
24
  /** Premier jour de la semaine : 0 = dimanche, 1 = lundi (défaut) */
17
25
  firstDayOfWeek?: number
18
- /** Raccourci « Today » sous la grille */
26
+ /** Libellé du raccourci « aujourd'hui » (texte selon `lang`) */
19
27
  todayBtn?: boolean
20
28
  /** En-tête : libellé cliquable ouvrant le choix du mois (et de l'année) */
21
29
  monthDropdown?: boolean
@@ -30,6 +38,11 @@ const props = withDefaults(defineProps<Props>(), {
30
38
 
31
39
  const emit = defineEmits<{ select: [date: Date] }>()
32
40
 
41
+ // Langue de l'app (`QConfigProvider lang`) : mois, jours et libellés suivent.
42
+ const configLang = useConfigLang()
43
+ const locale = computed(() => dateLocale(configLang.value))
44
+ const labels = computed(() => dateLabels(configLang.value))
45
+
33
46
  const today = new Date()
34
47
  const viewYear = ref(props.modelValue?.getFullYear() ?? today.getFullYear())
35
48
  const viewMonth = ref(props.modelValue?.getMonth() ?? today.getMonth())
@@ -51,10 +64,7 @@ const startOfDay = (d: Date) => new Date(d.getFullYear(), d.getMonth(), d.getDat
51
64
  const sameDay = (a: Date, b: Date) =>
52
65
  a.getFullYear() === b.getFullYear() && a.getMonth() === b.getMonth() && a.getDate() === b.getDate()
53
66
 
54
- const weekdayLabels = computed(() => {
55
- const names = ["Dim", "Lun", "Mar", "Mer", "Jeu", "Ven", "Sam"] as const
56
- return Array.from({ length: 7 }, (_, i) => names[(props.firstDayOfWeek + i) % 7])
57
- })
67
+ const weekdayLabels = computed(() => weekdayShortNames(locale.value, props.firstDayOfWeek))
58
68
 
59
69
  const weeks = computed<(Date | null)[][]>(() => {
60
70
  const y = viewYear.value
@@ -70,18 +80,10 @@ const weeks = computed<(Date | null)[][]>(() => {
70
80
  return result
71
81
  })
72
82
 
73
- const monthLabel = computed(() =>
74
- new Intl.DateTimeFormat(undefined, { month: "long", year: "numeric" }).format(
75
- new Date(viewYear.value, viewMonth.value, 1),
76
- ),
77
- )
83
+ const monthLabel = computed(() => monthYearLabel(locale.value, viewYear.value, viewMonth.value))
78
84
 
79
- /** Noms courts des 12 mois, dans la langue du navigateur (comme `monthLabel`) */
80
- const monthNames = computed(() =>
81
- Array.from({ length: 12 }, (_, i) =>
82
- new Intl.DateTimeFormat(undefined, { month: "short" }).format(new Date(2000, i, 1)),
83
- ),
84
- )
85
+ /** Noms courts des 12 mois, dans la langue de l'app (comme `monthLabel`) */
86
+ const monthNames = computed(() => monthShortNames(locale.value))
85
87
 
86
88
  const prevMonth = () => {
87
89
  if (viewMonth.value === 0) {
@@ -126,7 +128,7 @@ const onSelectDate = (d: Date) => {
126
128
  emit("select", d)
127
129
  }
128
130
 
129
- /** Raccourci « Today » : désactivé quand aujourd'hui est hors bornes / désactivé */
131
+ /** Raccourci « aujourd'hui » : désactivé quand aujourd'hui est hors bornes / désactivé */
130
132
  const todaySelectable = computed(() => !isDisabled(startOfDay(new Date())))
131
133
 
132
134
  const selectToday = () => {
@@ -141,7 +143,7 @@ const selectToday = () => {
141
143
  <button
142
144
  type="button"
143
145
  class="q-date-calendar__nav-btn"
144
- aria-label="Mois précédent"
146
+ :aria-label="labels.prevMonth"
145
147
  @click="prevMonth"
146
148
  >
147
149
  <Icon :icon="icons.chevronLeft" aria-hidden="true" />
@@ -152,7 +154,7 @@ const selectToday = () => {
152
154
  class="q-date-calendar__nav-label q-date-calendar__nav-label--button"
153
155
  :class="{ 'q-date-calendar__nav-label--open': monthPickerOpen }"
154
156
  :aria-expanded="monthPickerOpen"
155
- aria-label="Choisir le mois"
157
+ :aria-label="labels.chooseMonth"
156
158
  @click="monthPickerOpen = !monthPickerOpen"
157
159
  >
158
160
  <span class="q-date-calendar__nav-text">{{ monthLabel }}</span>
@@ -162,7 +164,7 @@ const selectToday = () => {
162
164
  <button
163
165
  type="button"
164
166
  class="q-date-calendar__nav-btn"
165
- aria-label="Mois suivant"
167
+ :aria-label="labels.nextMonth"
166
168
  @click="nextMonth"
167
169
  >
168
170
  <Icon :icon="icons.chevronRight" aria-hidden="true" />
@@ -175,7 +177,7 @@ const selectToday = () => {
175
177
  <button
176
178
  type="button"
177
179
  class="q-date-calendar__nav-btn"
178
- aria-label="Année précédente"
180
+ :aria-label="labels.prevYear"
179
181
  @click="prevYear"
180
182
  >
181
183
  <Icon :icon="icons.chevronLeft" aria-hidden="true" />
@@ -184,7 +186,7 @@ const selectToday = () => {
184
186
  <button
185
187
  type="button"
186
188
  class="q-date-calendar__nav-btn"
187
- aria-label="Année suivante"
189
+ :aria-label="labels.nextYear"
188
190
  @click="nextYear"
189
191
  >
190
192
  <Icon :icon="icons.chevronRight" aria-hidden="true" />
@@ -239,7 +241,7 @@ const selectToday = () => {
239
241
  :disabled="!todaySelectable"
240
242
  @click="selectToday"
241
243
  >
242
- Today
244
+ {{ labels.today }}
243
245
  </button>
244
246
  </div>
245
247
  </div>
package/index.ts CHANGED
@@ -164,6 +164,22 @@ export { screen } from "./lib/screen"
164
164
  export type { QScreen } from "./lib/screen"
165
165
  export { loading } from "./lib/loading"
166
166
  export type { LoadingOptions, QLoadingPlugin } from "./lib/loading"
167
+ export { network, createNetwork, resolveOnline, onlineAfterEvent } from "./lib/network"
168
+ export type { QNetwork, NetworkEvent, NetworkOptions, NetworkEventTarget, NetworkNavigator } from "./lib/network"
169
+ export {
170
+ wakeLock,
171
+ createWakeLock,
172
+ isWakeLockCapable,
173
+ isDocumentVisible,
174
+ shouldReacquire,
175
+ } from "./lib/wakeLock"
176
+ export type {
177
+ QWakeLock,
178
+ WakeLockOptions,
179
+ WakeLockApiLike,
180
+ WakeLockSentinelLike,
181
+ WakeLockVisibilityLike,
182
+ } from "./lib/wakeLock"
167
183
  export { localStorage, sessionStorage } from "./lib/storage"
168
184
  export type { QWebStorage } from "./lib/storage"
169
185
  export {
@@ -0,0 +1,91 @@
1
+ // Libellés / locale des composants de date — pilotés par le `lang` du provider.
2
+ import { describe, expect, it } from "bun:test"
3
+ import {
4
+ dateLabels,
5
+ dateLocale,
6
+ monthShortNames,
7
+ monthYearLabel,
8
+ weekdayShortNames,
9
+ } from "./dateI18n"
10
+
11
+ describe("dateLocale", () => {
12
+ it("mappe la langue de l'app sur une locale BCP 47 déterministe", () => {
13
+ expect(dateLocale("fr")).toBe("fr-FR")
14
+ expect(dateLocale("en")).toBe("en-US")
15
+ })
16
+ })
17
+
18
+ describe("dateLabels", () => {
19
+ it("« Today » devient « Aujourd'hui » en français", () => {
20
+ expect(dateLabels("fr").today).toBe("Aujourd'hui")
21
+ expect(dateLabels("en").today).toBe("Today")
22
+ })
23
+
24
+ it("couvre les libellés du panneau et de la navigation", () => {
25
+ expect(dateLabels("fr").selectDate).toBe("Sélectionner une date")
26
+ expect(dateLabels("fr").clear).toBe("Effacer")
27
+ expect(dateLabels("fr").close).toBe("Fermer")
28
+ expect(dateLabels("fr").prevMonth).toBe("Mois précédent")
29
+ expect(dateLabels("fr").nextMonth).toBe("Mois suivant")
30
+ expect(dateLabels("en").selectDate).toBe("Select a date")
31
+ })
32
+
33
+ it("retombe sur l'anglais pour une langue inconnue (JS non typé)", () => {
34
+ expect(dateLabels("de" as never).today).toBe("Today")
35
+ })
36
+ })
37
+
38
+ describe("weekdayShortNames", () => {
39
+ it("suit la locale demandée, pas celle du navigateur", () => {
40
+ expect(weekdayShortNames("fr-FR", 1)[0]).toMatch(/^lun/i)
41
+ expect(weekdayShortNames("en-US", 1)[0]).toMatch(/^mon/i)
42
+ })
43
+
44
+ it("commence par le jour demandé (0 = dimanche)", () => {
45
+ const monday = weekdayShortNames("en-US", 1)
46
+ const sunday = weekdayShortNames("en-US", 0)
47
+ expect(sunday[0]).toMatch(/^sun/i)
48
+ expect(sunday[0]).toBe(monday[6])
49
+ expect(sunday[6]).toBe(monday[5])
50
+ })
51
+
52
+ it("rend 7 jours sans point final ni minuscule initiale", () => {
53
+ const names = weekdayShortNames("fr-FR")
54
+ expect(names).toHaveLength(7)
55
+ for (const name of names) {
56
+ expect(name.endsWith(".")).toBe(false)
57
+ expect(name[0]).toBe(name[0]!.toUpperCase())
58
+ }
59
+ })
60
+ })
61
+
62
+ describe("monthShortNames", () => {
63
+ it("rend les 12 mois dans la langue demandée", () => {
64
+ const fr = monthShortNames("fr-FR")
65
+ const en = monthShortNames("en-US")
66
+ expect(fr).toHaveLength(12)
67
+ expect(en).toEqual([
68
+ "Jan",
69
+ "Feb",
70
+ "Mar",
71
+ "Apr",
72
+ "May",
73
+ "Jun",
74
+ "Jul",
75
+ "Aug",
76
+ "Sep",
77
+ "Oct",
78
+ "Nov",
79
+ "Dec",
80
+ ])
81
+ expect(fr[0]).toMatch(/^janv/i)
82
+ expect(fr[11]).toMatch(/^déc/i)
83
+ })
84
+ })
85
+
86
+ describe("monthYearLabel", () => {
87
+ it("en-tête long dans la langue demandée", () => {
88
+ expect(monthYearLabel("en-US", 2026, 0)).toBe("January 2026")
89
+ expect(monthYearLabel("fr-FR", 2026, 0)).toContain("janvier 2026")
90
+ })
91
+ })
@@ -0,0 +1,85 @@
1
+ // Libellés et locale des composants de date (QDatePicker / QDateCalendar).
2
+ //
3
+ // La langue vient du `QConfigProvider` (`lang`, défaut "en") et arrive ici via
4
+ // `useConfigLang()`. On ne s'appuie **pas** sur la locale du navigateur : le rendu
5
+ // doit être déterministe (serveur ↔ client) et suivre la langue choisie dans l'app.
6
+ import type { QAppLang } from "./config"
7
+
8
+ /** Clés d'interface des composants de date. */
9
+ export type DateUiKey =
10
+ | "today"
11
+ | "prevMonth"
12
+ | "nextMonth"
13
+ | "chooseMonth"
14
+ | "prevYear"
15
+ | "nextYear"
16
+ | "selectDate"
17
+ | "clear"
18
+ | "close"
19
+
20
+ const DATE_UI: Record<QAppLang, Record<DateUiKey, string>> = {
21
+ en: {
22
+ today: "Today",
23
+ prevMonth: "Previous month",
24
+ nextMonth: "Next month",
25
+ chooseMonth: "Choose month",
26
+ prevYear: "Previous year",
27
+ nextYear: "Next year",
28
+ selectDate: "Select a date",
29
+ clear: "Clear",
30
+ close: "Close",
31
+ },
32
+ fr: {
33
+ today: "Aujourd'hui",
34
+ prevMonth: "Mois précédent",
35
+ nextMonth: "Mois suivant",
36
+ chooseMonth: "Choisir le mois",
37
+ prevYear: "Année précédente",
38
+ nextYear: "Année suivante",
39
+ selectDate: "Sélectionner une date",
40
+ clear: "Effacer",
41
+ close: "Fermer",
42
+ },
43
+ }
44
+
45
+ /** Locale BCP 47 associée à la langue de l'app (déterministe, contrairement à `undefined`). */
46
+ export function dateLocale(lang: QAppLang): string {
47
+ return lang === "fr" ? "fr-FR" : "en-US"
48
+ }
49
+
50
+ /** Libellés d'interface des composants de date. */
51
+ export function dateLabels(lang: QAppLang): Record<DateUiKey, string> {
52
+ return DATE_UI[lang] ?? DATE_UI.en
53
+ }
54
+
55
+ /** Abréviation : initiale en majuscule, sans point final (`dim.` → `Dim`). */
56
+ function shortName(value: string): string {
57
+ const clean = value.replace(/[.,]+$/, "").trim()
58
+ return clean.charAt(0).toUpperCase() + clean.slice(1)
59
+ }
60
+
61
+ /**
62
+ * Jours de la semaine abrégés, en commençant par `firstDayOfWeek`
63
+ * (0 = dimanche, 1 = lundi).
64
+ */
65
+ export function weekdayShortNames(locale: string, firstDayOfWeek = 1): string[] {
66
+ const fmt = new Intl.DateTimeFormat(locale, { weekday: "short" })
67
+ // 2024-01-07 est un dimanche — référence stable, indépendante de la date du jour.
68
+ const names = Array.from({ length: 7 }, (_, i) =>
69
+ shortName(fmt.format(new Date(2024, 0, 7 + i))),
70
+ )
71
+ const start = ((firstDayOfWeek % 7) + 7) % 7
72
+ return Array.from({ length: 7 }, (_, i) => names[(start + i) % 7]!)
73
+ }
74
+
75
+ /** Mois abrégés, dans l'ordre de l'année (`Janv.` / `Jan`). */
76
+ export function monthShortNames(locale: string): string[] {
77
+ const fmt = new Intl.DateTimeFormat(locale, { month: "short" })
78
+ return Array.from({ length: 12 }, (_, i) => shortName(fmt.format(new Date(2024, i, 1))))
79
+ }
80
+
81
+ /** En-tête du calendrier : « janvier 2026 » / « January 2026 ». */
82
+ export function monthYearLabel(locale: string, year: number, month: number): string {
83
+ return new Intl.DateTimeFormat(locale, { month: "long", year: "numeric" })
84
+ .format(new Date(year, month, 1))
85
+ }
@@ -0,0 +1,199 @@
1
+ // $q.network — état de connexion : logique pure + fabrique testable hors DOM.
2
+ // (La règle projet interdit le navigateur : on injecte `navigator` et la cible d'événements.)
3
+ import { describe, expect, it } from "bun:test"
4
+ import { effect } from "vue"
5
+ import { createNetwork, onlineAfterEvent, resolveOnline } from "./network"
6
+
7
+ /** Fausse cible d'événements (window) + compteur d'écouteurs. */
8
+ function fakeTarget() {
9
+ const listeners = new Map<string, Set<(event: { type: string }) => void>>()
10
+ return {
11
+ addEventListener(type: string, cb: (event: { type: string }) => void) {
12
+ if (!listeners.has(type)) listeners.set(type, new Set())
13
+ listeners.get(type)!.add(cb)
14
+ },
15
+ removeEventListener(type: string, cb: (event: { type: string }) => void) {
16
+ listeners.get(type)?.delete(cb)
17
+ },
18
+ /** Émet un événement comme le navigateur. */
19
+ emit(type: string) {
20
+ for (const cb of [...(listeners.get(type) ?? [])]) cb({ type })
21
+ },
22
+ /** Nombre d'écouteurs encore attachés pour ce type. */
23
+ count(type: string) {
24
+ return listeners.get(type)?.size ?? 0
25
+ },
26
+ }
27
+ }
28
+
29
+ describe("resolveOnline", () => {
30
+ it("considère « en ligne » quand l'information manque (serveur, navigateur sans API)", () => {
31
+ expect(resolveOnline(undefined)).toBe(true)
32
+ expect(resolveOnline(null)).toBe(true)
33
+ expect(resolveOnline({})).toBe(true)
34
+ })
35
+
36
+ it("suit navigator.onLine quand il est un booléen", () => {
37
+ expect(resolveOnline({ onLine: true })).toBe(true)
38
+ expect(resolveOnline({ onLine: false })).toBe(false)
39
+ })
40
+ })
41
+
42
+ describe("onlineAfterEvent", () => {
43
+ it("online → connecté, offline → déconnecté", () => {
44
+ expect(onlineAfterEvent("online", false)).toBe(true)
45
+ expect(onlineAfterEvent("offline", true)).toBe(false)
46
+ })
47
+
48
+ it("un autre événement ne change rien", () => {
49
+ expect(onlineAfterEvent("focus", false)).toBe(false)
50
+ expect(onlineAfterEvent("focus", true)).toBe(true)
51
+ })
52
+ })
53
+
54
+ describe("createNetwork", () => {
55
+ it("lit l'état initial dans navigator.onLine", () => {
56
+ const nav = { onLine: false }
57
+ const net = createNetwork({ navigator: nav, target: null })
58
+ expect(net.online).toBe(false)
59
+ expect(net.offline).toBe(true)
60
+ })
61
+
62
+ it("sans cible (SSR), aucune écoute n'est posée et l'état reste stable", () => {
63
+ const net = createNetwork({ navigator: null, target: null })
64
+ expect(net.online).toBe(true)
65
+ })
66
+
67
+ it("suit les événements online / offline de la cible", () => {
68
+ const target = fakeTarget()
69
+ const net = createNetwork({ navigator: { onLine: true }, target })
70
+
71
+ target.emit("offline")
72
+ expect(net.online).toBe(false)
73
+ expect(net.offline).toBe(true)
74
+
75
+ target.emit("online")
76
+ expect(net.online).toBe(true)
77
+ expect(net.offline).toBe(false)
78
+ })
79
+
80
+ it("est réactif : un effet est rejoué au changement (template `$q.network.online`)", () => {
81
+ const target = fakeTarget()
82
+ const net = createNetwork({ navigator: { onLine: true }, target })
83
+ const seen: boolean[] = []
84
+
85
+ effect(() => {
86
+ seen.push(net.online)
87
+ })
88
+
89
+ target.emit("offline")
90
+ target.emit("online")
91
+
92
+ expect(seen).toEqual([true, false, true])
93
+ })
94
+
95
+ it("ne notifie qu'un vrai changement d'état", () => {
96
+ const target = fakeTarget()
97
+ const net = createNetwork({ navigator: { onLine: true }, target })
98
+ let offlineCalls = 0
99
+ let onlineCalls = 0
100
+ net.on("offline", () => offlineCalls++)
101
+ net.on("online", () => onlineCalls++)
102
+
103
+ target.emit("offline")
104
+ target.emit("offline")
105
+ target.emit("online")
106
+ target.emit("online")
107
+
108
+ expect(offlineCalls).toBe(1)
109
+ expect(onlineCalls).toBe(1)
110
+ })
111
+
112
+ it("on() cible le bon événement et rend un désabonnement", () => {
113
+ const target = fakeTarget()
114
+ const net = createNetwork({ navigator: { onLine: true }, target })
115
+ const seen: string[] = []
116
+ const stopOnline = net.on("online", () => seen.push("online"))
117
+ net.on("offline", () => seen.push("offline"))
118
+
119
+ target.emit("offline")
120
+ target.emit("online")
121
+ stopOnline()
122
+ target.emit("offline")
123
+ target.emit("online")
124
+
125
+ expect(seen).toEqual(["offline", "online", "offline"])
126
+ })
127
+
128
+ it("on() ignore un type inconnu (JS non typé)", () => {
129
+ const target = fakeTarget()
130
+ const net = createNetwork({ navigator: { onLine: true }, target })
131
+ const stop = net.on("focus" as never, () => {
132
+ throw new Error("ne doit jamais être appelé")
133
+ })
134
+
135
+ target.emit("offline")
136
+ expect(typeof stop).toBe("function")
137
+ stop()
138
+ })
139
+
140
+ it("les abonnements se désabonnent", () => {
141
+ const target = fakeTarget()
142
+ const net = createNetwork({ navigator: { onLine: true }, target })
143
+ let calls = 0
144
+ const off = net.on("offline", () => calls++)
145
+
146
+ target.emit("offline")
147
+ off()
148
+ target.emit("online")
149
+ target.emit("offline")
150
+
151
+ expect(calls).toBe(1)
152
+ })
153
+
154
+ it("un abonné qui jette ne bloque pas les suivants", () => {
155
+ const target = fakeTarget()
156
+ const net = createNetwork({ navigator: { onLine: true }, target })
157
+ let calls = 0
158
+ net.on("offline", () => {
159
+ throw new Error("boom")
160
+ })
161
+ net.on("offline", () => calls++)
162
+
163
+ target.emit("offline")
164
+
165
+ expect(calls).toBe(1)
166
+ })
167
+
168
+ it("refresh() relit navigator.onLine (valeur devenue obsolète)", () => {
169
+ const nav = { onLine: true }
170
+ const net = createNetwork({ navigator: nav, target: fakeTarget() })
171
+
172
+ nav.onLine = false
173
+ expect(net.refresh()).toBe(false)
174
+ expect(net.online).toBe(false)
175
+
176
+ nav.onLine = true
177
+ expect(net.online).toBe(false) // pas de sondage automatique
178
+ expect(net.refresh()).toBe(true)
179
+ expect(net.online).toBe(true)
180
+ })
181
+
182
+ it("destroy() détache les écouteurs et vide les abonnements", () => {
183
+ const target = fakeTarget()
184
+ const net = createNetwork({ navigator: { onLine: true }, target })
185
+ let calls = 0
186
+ net.on("offline", () => calls++)
187
+
188
+ expect(target.count("online")).toBe(1)
189
+ expect(target.count("offline")).toBe(1)
190
+
191
+ net.destroy()
192
+ target.emit("offline")
193
+
194
+ expect(target.count("online")).toBe(0)
195
+ expect(target.count("offline")).toBe(0)
196
+ expect(net.online).toBe(true)
197
+ expect(calls).toBe(0)
198
+ })
199
+ })