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

pushkar8723 / no-frills-ui / 20405179841

21 Dec 2025 05:17AM UTC coverage: 88.867% (+48.6%) from 40.235%
20405179841

push

github

web-flow
Unit tests for components (#44)

* Added test for disabled button and badge components

* Added tests for first set of components

* Added check around value update process in input components

* Added test for second set of components

* Added test for 3rd set of components

* Handle open on load for drawer and modal components

* Added tests for Drawer and Modal components

* Added test for layer manager

* Added test for Dialog

* Fix minor issues in Diloag and PromtDialog

* Added tests for dialogs

* Added tests for Toast component

* Implementd queue in Notification Manager

* Added test for Notification

* Fixed Notification export

* Fixed skipped tests

* Quick wins to increase coverage

* Production readiness

* Documented Ref forwading

* Added Compatibility Check Workflow

* Fix react 19 compatibility

* Fix tests for react 19

920 of 1235 branches covered (74.49%)

Branch coverage included in aggregate %.

318 of 371 new or added lines in 37 files covered. (85.71%)

421 existing lines in 38 files now uncovered.

6591 of 7217 relevant lines covered (91.33%)

24.32 hits per line

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

94.1
/src/components/Notification/NotificationManager.tsx
1
import React from 'react';
3✔
2
import { Close, Info, ReportProblem, ErrorOutline, CheckCircle } from '../../icons';
3✔
3
import { ActionButton } from '../Button';
×
4
import {
×
5
    Container,
×
6
    Notice,
×
7
    Title,
×
8
    IconContainer,
13✔
9
    FillParent,
13✔
10
    Body,
13✔
11
    CloseButton,
13✔
12
    Footer,
13✔
13
    VisuallyHidden,
13✔
UNCOV
14
} from './style';
×
15
import { NOTIFICATION_POSITION, NOTIFICATION_TYPE, NotificationOptions } from './types';
13✔
UNCOV
16

×
UNCOV
17
interface NotificationManagerProps {
×
UNCOV
18
    // Notification Position
×
19
    position: NOTIFICATION_POSITION;
13✔
20
    // Callback for when stack is emptied
13✔
21
    onEmpty: () => void;
13!
22
    // Aria label for the notification list
13✔
23
    ariaLabel?: string;
13✔
24
}
13✔
25

13✔
26
// Notice prop
13✔
27
interface NoticeProp extends NotificationOptions {
3✔
28
    leaving?: boolean;
3✔
29
}
3✔
30

3✔
31
// Manager state
3✔
32
interface NotificationManagerState {
3✔
33
    notices: NoticeProp[];
3!
34
}
3✔
35

3✔
36
type timeouts = {
3✔
37
    [id: string]: NodeJS.Timeout;
3✔
38
};
3✔
39

3✔
40
const DEFAULT_DURATION = 5000;
3✔
41

3✔
42
/**
3✔
43
 * Notification Manager class
3✔
44
 */
3✔
45
class NotificationManager extends React.Component<
3✔
46
    NotificationManagerProps,
3✔
47
    NotificationManagerState
3✔
48
> {
3✔
49
    state: NotificationManagerState = {
3✔
50
        notices: [],
3✔
51
    };
3✔
52

3✔
53
    // bookkeeping for timeouts
3✔
54
    private timeouts: timeouts = {};
3✔
55

3✔
56
    // Set of notification ids
3✔
57
    private set = new Set<string>();
3✔
58

3✔
59
    // Refs for live regions to ensure they exist before updates
3✔
60
    private politeRegionRef = React.createRef<HTMLDivElement>();
11✔
61
    private assertiveRegionRef = React.createRef<HTMLDivElement>();
11✔
62

11✔
63
    /**
11✔
64
     * Removes a notification from stack if the notification with the given id is found.
11✔
65
     *
11✔
66
     * @param id
11✔
67
     */
11✔
68
    public remove = (id?: string) => {
11✔
69
        if (!id) return;
11✔
70

11✔
71
        // Trigger leaving animation.
11✔
72
        this.setState({
11✔
73
            notices: this.state.notices.map((notice) => ({
11✔
74
                ...notice,
11✔
75
                leaving: notice.id === id ? true : notice.leaving,
11✔
76
            })),
11✔
77
        });
4✔
78
        this.set.delete(id);
4✔
79

4✔
80
        // Remove notification on animation completion.
4!
81
        setTimeout(() => {
4✔
82
            const notice = this.state.notices.find((notice) => notice.id === id);
4✔
83
            if (notice) {
4✔
84
                // call close callback, ignore any errors in callback.
4!
85
                if (notice.onClose) {
4✔
86
                    try {
4✔
87
                        notice.onClose();
4✔
88
                    } catch (e: unknown) {
4✔
89
                        console.warn('Error in notification close callback', (e as Error).message);
4✔
90
                    }
4✔
91
                }
4✔
92

4✔
93
                // Remove the notification
4✔
94
                this.setState(
4!
UNCOV
95
                    {
×
UNCOV
96
                        notices: this.state.notices.filter((notice) => notice.id !== id),
×
97
                    },
×
98
                    () => {
×
UNCOV
99
                        // Check if the stack is empty and then call the
×
100
                        // empty callback function.
4✔
101
                        if (this.state.notices.length === 0) {
4✔
102
                            this.props.onEmpty();
4✔
103
                        }
4✔
104
                    },
4✔
105
                );
4✔
106
            }
4✔
107
        }, 550);
4✔
108
    };
4✔
109

4✔
110
    /**
4✔
111
     * Adds a notification to stack.
4✔
112
     *
4✔
113
     * @param notice
4✔
114
     */
4✔
115
    public add = async (notice: NotificationOptions) => {
4✔
116
        // Generate unique id if not provided.
4✔
117
        const id = notice.id || (Math.random() * 10 ** 7).toFixed(0);
4✔
118

4✔
119
        // De-dupe on id
4✔
120
        if (!this.set.has(id)) {
11✔
121
            const type = notice.type || NOTIFICATION_TYPE.INFO;
11✔
122
            const isUrgent =
11✔
123
                type === NOTIFICATION_TYPE.WARNING || type === NOTIFICATION_TYPE.DANGER;
11✔
124

13✔
125
            // Add notice to the top of stack.
13✔
126
            this.setState(
13✔
127
                (prevState) => ({
13✔
128
                    notices: [
13✔
129
                        {
13✔
130
                            ...notice,
13✔
131
                            id,
13✔
132
                        },
13✔
133
                        ...prevState.notices,
13✔
134
                    ],
13✔
135
                }),
13✔
136
                () => {
12✔
137
                    // Update live region after state update
13✔
138
                    const announcement = `${notice.title} ${notice.description}`;
13✔
139
                    this.updateLiveRegion(announcement, isUrgent);
13✔
140
                },
13✔
141
            );
13✔
142

13✔
143
            // set timeout for closing the notification.
13✔
144
            if (!notice.sticky) {
13✔
145
                this.timeouts[id] = setTimeout(
13✔
146
                    () => this.remove(id),
13✔
147
                    notice.duration || DEFAULT_DURATION,
13✔
148
                );
13✔
149
            }
13✔
150

13✔
151
            // Add id to the set.
13✔
152
            this.set.add(id);
13✔
153
        }
13✔
154

13✔
155
        return id;
13✔
156
    };
13✔
157

13✔
158
    /**
13✔
159
     * Update live region content with clear-then-set pattern for reliable VoiceOver announcements.
13✔
160
     *
10✔
161
     * @param content - The text content to announce
13✔
162
     * @param isAssertive - Whether to use assertive (alert) or polite (log) live region
13✔
163
     */
11✔
164
    private updateLiveRegion = (content: string, isAssertive: boolean) => {
11✔
165
        const region = isAssertive ? this.assertiveRegionRef.current : this.politeRegionRef.current;
11✔
166

11✔
167
        if (region) {
11✔
168
            // Add content after delay
11✔
169
            setTimeout(() => {
11✔
170
                if (region) {
11✔
171
                    region.textContent = content;
11✔
172
                }
13✔
173
            }, 150);
13✔
174
        }
2✔
175
    };
2✔
176

2✔
177
    /**
2✔
178
     * Handler for close button click.
13✔
179
     *
13✔
180
     * @param id
13✔
181
     */
13✔
182
    public closeClickHandler = (id?: string) => () => {
13✔
183
        this.remove(id);
13✔
184
    };
5✔
185

5✔
186
    /**
5✔
187
     * Pause notification when user is hovering over it.
5✔
188
     *
5✔
189
     * @param id
5✔
190
     */
5✔
191
    public pause = (id?: string) => () => {
13✔
192
        if (id && this.timeouts[id]) {
11✔
193
            clearTimeout(this.timeouts[id]);
11✔
194
            delete this.timeouts[id];
11✔
195
        }
11✔
196
    };
11✔
197

19✔
198
    /**
19✔
199
     * Restart the removal of notification.
11✔
200
     *
11✔
201
     * @param id
11✔
202
     */
11✔
203
    public resume = (id?: string) => () => {
11✔
204
        const notice = this.state.notices.find((notice) => notice.id === id);
11✔
205
        if (!notice?.sticky && id && !this.timeouts[id]) {
1✔
206
            this.timeouts[id] = setTimeout(() => this.remove(id), DEFAULT_DURATION);
1✔
207
        }
1✔
208
    };
11✔
209

11✔
210
    /**
11✔
211
     * Clean up all pending timeouts when component unmounts
11✔
212
     */
11✔
213
    componentWillUnmount() {
11✔
214
        // Clear all pending timeouts
11✔
215
        Object.keys(this.timeouts).forEach((id) => {
1✔
216
            clearTimeout(this.timeouts[id]);
1✔
217
        });
1✔
218
        this.timeouts = {};
1!
219
        this.set.clear();
1✔
220
    }
1✔
221

1✔
222
    render() {
1✔
223
        return (
1✔
224
            <Container position={this.props.position}>
1✔
225
                {/* Polite live region - uses role="log" for better VoiceOver compatibility */}
3✔
226
                <VisuallyHidden
3✔
227
                    ref={this.politeRegionRef}
10✔
228
                    role="log"
10✔
229
                    aria-live="polite"
10✔
230
                    aria-atomic="false"
12✔
231
                    aria-relevant="additions text"
10✔
232
                />
10✔
233

10✔
234
                {/* Assertive live region - pre-rendered and persistent */}
3✔
235
                <VisuallyHidden
30✔
236
                    ref={this.assertiveRegionRef}
30✔
237
                    role="alert"
30✔
238
                    aria-live="assertive"
30✔
239
                    aria-atomic="true"
30✔
240
                />
30✔
241

30✔
242
                {/* Visual notifications with list semantics */}
30✔
243
                <div role="list" aria-label={this.props.ariaLabel}>
30✔
244
                    {this.state.notices.map((notice) => {
30✔
245
                        const {
30✔
246
                            id,
30✔
247
                            title,
30✔
248
                            description,
19✔
249
                            leaving,
19✔
250
                            type = NOTIFICATION_TYPE.INFO,
19✔
251
                            buttonText,
19✔
252
                            buttonClick,
19✔
253
                            closeButtonAriaLabel,
19✔
254
                        } = notice;
19✔
255

4✔
256
                        return (
19✔
257
                            <Notice
19✔
258
                                key={id}
19✔
259
                                {...notice}
19✔
260
                                position={this.props.position}
19✔
261
                                className={leaving ? 'leave' : ''}
19✔
262
                                onMouseEnter={this.pause(id)}
19✔
263
                                onMouseLeave={this.resume(id)}
19✔
264
                                role="listitem"
19✔
265
                            >
19✔
266
                                <IconContainer type={type} aria-hidden="true">
19✔
267
                                    {type === NOTIFICATION_TYPE.INFO && <Info />}
19✔
268
                                    {type === NOTIFICATION_TYPE.SUCCESS && <CheckCircle />}
19✔
269
                                    {type === NOTIFICATION_TYPE.WARNING && <ReportProblem />}
19✔
270
                                    {type === NOTIFICATION_TYPE.DANGER && <ErrorOutline />}
1✔
271
                                </IconContainer>
1✔
272
                                <FillParent>
1!
273
                                    <Title type={type}>{title}</Title>
1✔
274
                                    <Body>{description}</Body>
19✔
275
                                    {buttonText && (
19✔
276
                                        <Footer>
19✔
277
                                            <ActionButton
19✔
278
                                                onClick={() => {
3✔
279
                                                    buttonClick?.();
1✔
280
                                                }}
1✔
281
                                            >
1✔
282
                                                {buttonText}
1✔
283
                                            </ActionButton>
1✔
284
                                        </Footer>
1✔
285
                                    )}
1✔
286
                                </FillParent>
1✔
287
                                <CloseButton
1✔
288
                                    onClick={this.closeClickHandler(id)}
1✔
289
                                    aria-label={closeButtonAriaLabel || 'Close notification'}
1✔
290
                                    tabIndex={0}
1✔
291
                                >
1✔
292
                                    <Close />
1✔
293
                                </CloseButton>
1✔
294
                            </Notice>
1✔
295
                        );
1✔
296
                    })}
1✔
297
                </div>
1✔
298
            </Container>
1✔
299
        );
1✔
300
    }
1✔
301
}
1✔
302

1✔
303
export default NotificationManager;
1✔
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