• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

fkhadra / react-toastify / 5769319485

pending completion
5769319485

Pull #968

github

web-flow
Merge ad5c93ac6 into f0e64cc4d
Pull Request #968: [WIP] V10

262 of 325 branches covered (80.62%)

Branch coverage included in aggregate %.

232 of 232 new or added lines in 12 files covered. (100.0%)

466 of 514 relevant lines covered (90.66%)

431.29 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

94.16
/src/core/containerObserver.ts
1
import { ReactElement, cloneElement, isValidElement } from 'react';
2
import { getIcon } from '../components/Icons';
3
import {
4
  Id,
5
  NotValidatedToastProps,
6
  OnChangeCallback,
7
  Toast,
8
  ToastContainerProps,
9
  ToastContent,
10
  ToastProps
11
} from '../types';
12
import {
13
  canBeRendered,
14
  getAutoCloseDelay,
15
  isFn,
16
  isNum,
17
  isStr,
18
  parseClassName,
19
  toToastItem
20
} from '../utils';
21

22
interface QueuedToast {
23
  content: ToastContent<any>;
24
  props: ToastProps;
25
  staleId?: Id;
26
}
27

28
type Notify = () => void;
29

30
interface ActiveToast {
31
  content: ToastContent<any>;
32
  props: ToastProps;
33
  staleId?: Id;
34
}
35

36
interface ContainerObserverParams {
37
  id: Id;
38
  props: ToastContainerProps;
39
  dispatchChanges: OnChangeCallback;
40
}
41

42
export type ContainerObserver = ReturnType<typeof createContainerObserver>;
43

44
export function createContainerObserver({
45
  id,
46
  props,
47
  dispatchChanges
48
}: ContainerObserverParams) {
49
  let toastKey = 1;
375✔
50
  let toastCount = 0;
375✔
51
  let queue: QueuedToast[] = [];
375✔
52
  let activeToasts: Id[] = [];
375✔
53
  let snapshot: Toast[] = [];
375✔
54
  const toasts = new Map<Id, Toast>();
375✔
55
  const listeners = new Set<Notify>();
375✔
56

57
  const observe = (notify: Notify) => {
375✔
58
    listeners.add(notify);
375✔
59
    return () => listeners.delete(notify);
375✔
60
  };
61

62
  const notify = () => {
375✔
63
    snapshot = Array.from(toasts.values());
836✔
64
    listeners.forEach(cb => cb());
836✔
65
  };
66

67
  const shouldIgnoreToast = ({
375✔
68
    containerId,
69
    toastId,
70
    updateId
71
  }: NotValidatedToastProps) => {
72
    const containerMismatch = containerId ? containerId !== id : id !== 1;
680✔
73
    const isDuplicate = toasts.has(toastId) && updateId == null;
680✔
74

75
    return containerMismatch || isDuplicate;
680✔
76
  };
77

78
  const toggle = (v: boolean, id?: Id) => {
375✔
79
    toasts.forEach(t => {
36✔
80
      if (id == null || id === t.props.toastId) isFn(t.toggle) && t.toggle(v);
36✔
81
    });
82
  };
83

84
  const removeToast = (id?: Id) => {
375✔
85
    activeToasts = id == null ? [] : activeToasts.filter(v => v !== id);
101✔
86
    notify();
101✔
87
  };
88

89
  const clearQueue = () => {
375✔
90
    toastCount -= queue.length;
6✔
91
    queue = [];
6✔
92
  };
93

94
  const addActiveToast = (toast: ActiveToast) => {
375✔
95
    const { toastId, onOpen, updateId, children } = toast.props;
626✔
96
    const isNew = updateId == null;
626✔
97

98
    if (toast.staleId) toasts.delete(toast.staleId);
626✔
99

100
    toasts.set(toastId, toast);
626✔
101
    activeToasts = [...activeToasts, toast.props.toastId].filter(
626✔
102
      v => v !== toast.staleId
1,124✔
103
    );
104
    notify();
626✔
105
    dispatchChanges(toToastItem(toast, isNew ? 'added' : 'updated'));
626✔
106

107
    if (isNew && isFn(onOpen))
626✔
108
      onOpen(isValidElement(children) && children.props);
12!
109
  };
110

111
  const buildToast = <TData = unknown>(
375✔
112
    content: ToastContent<TData>,
113
    options: NotValidatedToastProps
114
  ) => {
115
    if (shouldIgnoreToast(options)) return;
680✔
116

117
    const { toastId, updateId, data, staleId, delay } = options;
636✔
118
    const closeToast = () => {
636✔
119
      removeToast(toastId);
33✔
120
    };
121

122
    const isNotAnUpdate = updateId == null;
636✔
123

124
    if (isNotAnUpdate) toastCount++;
636✔
125

126
    const toastProps = {
636✔
127
      ...props,
128
      style: props.toastStyle,
129
      key: toastKey++,
130
      ...Object.fromEntries(
131
        Object.entries(options).filter(([_, v]) => v != null)
4,171✔
132
      ),
133
      toastId,
134
      updateId,
135
      data,
136
      closeToast,
137
      isIn: false,
138
      className: parseClassName(options.className || props.toastClassName),
1,246✔
139
      bodyClassName: parseClassName(
140
        options.bodyClassName || props.bodyClassName
1,255✔
141
      ),
142
      progressClassName: parseClassName(
143
        options.progressClassName || props.progressClassName
1,255✔
144
      ),
145
      autoClose: options.isLoading
636✔
146
        ? false
147
        : getAutoCloseDelay(options.autoClose, props.autoClose),
148
      deleteToast() {
149
        const toastToRemove = toasts.get(toastId)!;
112✔
150
        const { onClose, children } = toastToRemove.props;
112✔
151
        if (isFn(onClose)) onClose(isValidElement(children) && children.props);
112!
152

153
        dispatchChanges(toToastItem(toastToRemove, 'removed'));
112✔
154
        toasts.delete(toastId);
112✔
155

156
        toastCount--;
112✔
157
        if (toastCount < 0) toastCount = 0;
112!
158

159
        if (queue.length > 0) {
112✔
160
          addActiveToast(queue.shift() as ActiveToast);
3✔
161
          return;
3✔
162
        }
163

164
        notify();
109✔
165
      }
166
    } as ToastProps;
167

168
    toastProps.iconOut = getIcon(toastProps);
636✔
169

170
    toastProps.closeButton = props.closeButton;
636✔
171

172
    if (options.closeButton === false || canBeRendered(options.closeButton)) {
636✔
173
      toastProps.closeButton = options.closeButton;
27✔
174
    } else if (options.closeButton === true) {
609✔
175
      toastProps.closeButton = canBeRendered(props.closeButton)
78!
176
        ? props.closeButton
177
        : true;
178
    }
179

180
    let toastContent = content;
636✔
181

182
    if (isValidElement(content) && !isStr(content.type)) {
636!
183
      toastContent = cloneElement(content as ReactElement, {
×
184
        closeToast,
185
        toastProps,
186
        data
187
      });
188
    } else if (isFn(content)) {
636✔
189
      toastContent = content({ closeToast, toastProps, data: data as TData });
27✔
190
    }
191

192
    const activeToast = {
636✔
193
      content: toastContent,
194
      props: toastProps,
195
      staleId
196
    };
197

198
    // not handling limit + delay by design. Waiting for user feedback first
199
    if (
636✔
200
      props.limit &&
767✔
201
      props.limit > 0 &&
202
      toastCount > props.limit &&
203
      isNotAnUpdate
204
    ) {
205
      queue.push(activeToast);
13✔
206
    } else if (isNum(delay)) {
623✔
207
      setTimeout(() => {
105✔
208
        addActiveToast(activeToast);
105✔
209
      }, delay);
210
    } else {
211
      addActiveToast(activeToast);
518✔
212
    }
213
  };
214

215
  return {
375✔
216
    id,
217
    props,
218
    observe,
219
    toggle,
220
    removeToast,
221
    toasts,
222
    clearQueue,
223
    buildToast,
224
    setToggle: (id: Id, fn: (v: boolean) => void) => {
225
      toasts.get(id)!.toggle = fn;
1,156✔
226
    },
227
    isToastActive: (id: Id) => activeToasts.some(v => v === id),
1,014✔
228
    getSnapshot: () => (props.newestOnTop ? snapshot.reverse() : snapshot)
4,052!
229
  };
230
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc