@planningcenter/sweetest-alert 0.1.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/.editorconfig +8 -0
- package/.github/CODEOWNERS +1 -0
- package/.github/workflows/pco-release-create-pr.yml +24 -0
- package/.github/workflows/pco-release-create-release-on-merge.yml +17 -0
- package/.github/workflows/pco-release-dependabot-automation.yml +15 -0
- package/.github/workflows/pco-release-require-changelog-update.yml +22 -0
- package/.github/workflows/pco-release-sync-release-by-label.yml +18 -0
- package/.stylelintrc.json +8 -0
- package/CHANGELOG.md +6 -0
- package/LICENSE +21 -0
- package/README.md +165 -0
- package/docs/demo.css +198 -0
- package/docs/demo.tsx +306 -0
- package/docs/index.html +13 -0
- package/eslint.config.js +56 -0
- package/package.json +62 -0
- package/src/index.ts +1 -0
- package/src/sweetest_alert.css +62 -0
- package/src/sweetest_alert.tsx +130 -0
- package/tests/sweetest_alert.test.jsx +254 -0
- package/tests/test-setup.ts +13 -0
- package/tsconfig.json +23 -0
- package/vite.config.ts +11 -0
- package/vitest.config.ts +11 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import React from "react"
|
|
2
|
+
import { screen, fireEvent, waitFor, act } from "@testing-library/react"
|
|
3
|
+
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"
|
|
4
|
+
import { SweetestAlert } from "../src/sweetest_alert.tsx"
|
|
5
|
+
|
|
6
|
+
describe("SweetestAlert", () => {
|
|
7
|
+
const mockConfirm = vi.fn()
|
|
8
|
+
const mockCancel = vi.fn()
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
vi.clearAllMocks()
|
|
12
|
+
document.querySelectorAll("dialog.pco-sweetest-alert").forEach((dialog) => dialog.remove())
|
|
13
|
+
})
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
document.querySelectorAll("dialog.pco-sweetest-alert").forEach((dialog) => dialog.remove())
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
describe("Initial Render", () => {
|
|
20
|
+
beforeEach(async () => {
|
|
21
|
+
await act(async () => {
|
|
22
|
+
SweetestAlert({
|
|
23
|
+
title: "Test Title",
|
|
24
|
+
content: "Test content",
|
|
25
|
+
confirm: mockConfirm,
|
|
26
|
+
cancel: mockCancel,
|
|
27
|
+
})
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
it("creates and displays a modal dialog", () => {
|
|
32
|
+
const dialog = document.querySelector("dialog.pco-sweetest-alert")
|
|
33
|
+
expect(dialog).toBeInTheDocument()
|
|
34
|
+
expect(dialog).toHaveAttribute("open")
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
it("displays the provided title as a heading", () => {
|
|
38
|
+
expect(screen.getByRole("heading", { name: "Test Title" })).toBeInTheDocument()
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
it("displays the content text in the dialog body", () => {
|
|
42
|
+
expect(screen.getByText("Test content")).toBeInTheDocument()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it("displays a cancel button", () => {
|
|
46
|
+
expect(screen.getByRole("button", { name: "Cancel" })).toBeInTheDocument()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it("displays a confirm button with default label", () => {
|
|
50
|
+
expect(screen.getByRole("button", { name: "Okay" })).toBeInTheDocument()
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
it("displays a warning icon when no type is specified", () => {
|
|
54
|
+
const icon = document.querySelector(".pco-sweetest-alert__icon.pco-sweetest-alert--warning")
|
|
55
|
+
expect(icon).toBeInTheDocument()
|
|
56
|
+
expect(icon.querySelector("path")).toBeInTheDocument()
|
|
57
|
+
})
|
|
58
|
+
})
|
|
59
|
+
|
|
60
|
+
describe("Alert Types", () => {
|
|
61
|
+
it("displays success icon when type is set to success", async () => {
|
|
62
|
+
await act(async () => {
|
|
63
|
+
SweetestAlert({
|
|
64
|
+
title: "Success",
|
|
65
|
+
content: "Success content",
|
|
66
|
+
type: "success",
|
|
67
|
+
})
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
const icon = document.querySelector(".pco-sweetest-alert__icon.pco-sweetest-alert--success")
|
|
71
|
+
expect(icon).toBeInTheDocument()
|
|
72
|
+
expect(icon.querySelector("path")).toBeInTheDocument()
|
|
73
|
+
})
|
|
74
|
+
|
|
75
|
+
it("displays error icon when type is set to error", async () => {
|
|
76
|
+
await act(async () => {
|
|
77
|
+
SweetestAlert({
|
|
78
|
+
title: "Error",
|
|
79
|
+
content: "Error content",
|
|
80
|
+
type: "error",
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
const icon = document.querySelector(".pco-sweetest-alert__icon.pco-sweetest-alert--error")
|
|
85
|
+
expect(icon).toBeInTheDocument()
|
|
86
|
+
expect(icon.querySelector("path")).toBeInTheDocument()
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it("displays info icon when type is set to info", async () => {
|
|
90
|
+
await act(async () => {
|
|
91
|
+
SweetestAlert({
|
|
92
|
+
title: "Info",
|
|
93
|
+
content: "Info content",
|
|
94
|
+
type: "info",
|
|
95
|
+
})
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
const icon = document.querySelector(".pco-sweetest-alert__icon.pco-sweetest-alert--info")
|
|
99
|
+
expect(icon).toBeInTheDocument()
|
|
100
|
+
expect(icon.querySelector("path")).toBeInTheDocument()
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it("displays red warning icon and delete button kind when type is set to danger", async () => {
|
|
104
|
+
await act(async () => {
|
|
105
|
+
SweetestAlert({
|
|
106
|
+
title: "Danger",
|
|
107
|
+
content: "Danger content",
|
|
108
|
+
type: "danger",
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
const icon = document.querySelector(".pco-sweetest-alert__icon.pco-sweetest-alert--danger")
|
|
113
|
+
expect(icon).toBeInTheDocument()
|
|
114
|
+
expect(icon.querySelector("path")).toBeInTheDocument()
|
|
115
|
+
|
|
116
|
+
const button = document.querySelector(".tds-btn.tds-btn--delete")
|
|
117
|
+
expect(button).toBeInTheDocument()
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
describe("Button Interactions", () => {
|
|
122
|
+
beforeEach(async () => {
|
|
123
|
+
await act(async () => {
|
|
124
|
+
SweetestAlert({
|
|
125
|
+
title: "Interactive Test",
|
|
126
|
+
content: "Test content",
|
|
127
|
+
confirm: mockConfirm,
|
|
128
|
+
cancel: mockCancel,
|
|
129
|
+
})
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
it("executes confirm callback and removes dialog when confirm button is clicked", async () => {
|
|
134
|
+
const confirmButton = screen.getByRole("button", { name: "Okay" })
|
|
135
|
+
fireEvent.click(confirmButton)
|
|
136
|
+
|
|
137
|
+
await waitFor(() => {
|
|
138
|
+
expect(mockConfirm).toHaveBeenCalledTimes(1)
|
|
139
|
+
})
|
|
140
|
+
|
|
141
|
+
// Dialog should be removed from DOM
|
|
142
|
+
await waitFor(() => {
|
|
143
|
+
expect(document.querySelector("dialog.pco-sweetest-alert")).not.toBeInTheDocument()
|
|
144
|
+
})
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it("executes cancel callback and removes dialog when cancel button is clicked", async () => {
|
|
148
|
+
const cancelButton = screen.getByRole("button", { name: "Cancel" })
|
|
149
|
+
fireEvent.click(cancelButton)
|
|
150
|
+
|
|
151
|
+
await waitFor(() => {
|
|
152
|
+
expect(mockCancel).toHaveBeenCalledTimes(1)
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
// Dialog should be removed from DOM
|
|
156
|
+
await waitFor(() => {
|
|
157
|
+
expect(document.querySelector("dialog.pco-sweetest-alert")).not.toBeInTheDocument()
|
|
158
|
+
})
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it("executes cancel callback and removes dialog when escape key is pressed", async () => {
|
|
162
|
+
const dialog = document.querySelector("dialog.pco-sweetest-alert")
|
|
163
|
+
|
|
164
|
+
// Simulate pressing Escape key which triggers the dialog's close event
|
|
165
|
+
fireEvent.keyDown(dialog, { key: "Escape", code: "Escape" })
|
|
166
|
+
|
|
167
|
+
// Manually trigger the close event since jsdom doesn't handle native dialog behavior
|
|
168
|
+
fireEvent(dialog, new Event("close"))
|
|
169
|
+
|
|
170
|
+
await waitFor(() => {
|
|
171
|
+
expect(mockCancel).toHaveBeenCalledTimes(1)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
// Dialog should be removed from DOM
|
|
175
|
+
await waitFor(() => {
|
|
176
|
+
expect(document.querySelector("dialog.pco-sweetest-alert")).not.toBeInTheDocument()
|
|
177
|
+
})
|
|
178
|
+
})
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
describe("Custom Button Labels", () => {
|
|
182
|
+
beforeEach(async () => {
|
|
183
|
+
await act(async () => {
|
|
184
|
+
SweetestAlert({
|
|
185
|
+
title: "Custom Buttons",
|
|
186
|
+
content: "Test content",
|
|
187
|
+
confirmButton: "Yes please",
|
|
188
|
+
confirm: mockConfirm,
|
|
189
|
+
cancel: mockCancel,
|
|
190
|
+
})
|
|
191
|
+
})
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
it("displays custom confirm button text when confirmButton prop is provided", () => {
|
|
195
|
+
expect(screen.getByRole("button", { name: "Yes please" })).toBeInTheDocument()
|
|
196
|
+
})
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
describe("Hide Cancel Button", () => {
|
|
200
|
+
beforeEach(async () => {
|
|
201
|
+
await act(async () => {
|
|
202
|
+
SweetestAlert({
|
|
203
|
+
title: "Custom Buttons",
|
|
204
|
+
content: "Test content",
|
|
205
|
+
confirm: mockConfirm,
|
|
206
|
+
cancel: mockCancel,
|
|
207
|
+
hideCancel: true,
|
|
208
|
+
})
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it("does not display cancel button when hideCancel is true", () => {
|
|
213
|
+
expect(screen.queryByRole("button", { name: "Cancel" })).not.toBeInTheDocument()
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
|
|
217
|
+
describe("Renders HTML instead of text content", () => {
|
|
218
|
+
it("renders content property as HTML", async () => {
|
|
219
|
+
await act(async () => {
|
|
220
|
+
const HtmlFrag = () => {
|
|
221
|
+
return <strong>Test content</strong>
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
SweetestAlert({
|
|
225
|
+
title: "Custom HTML",
|
|
226
|
+
content: <HtmlFrag />,
|
|
227
|
+
confirm: mockConfirm,
|
|
228
|
+
cancel: mockCancel,
|
|
229
|
+
})
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
expect(screen.getByText("Test content")).toBeInTheDocument()
|
|
233
|
+
expect(screen.getByText("Test content").tagName).toBe("STRONG")
|
|
234
|
+
})
|
|
235
|
+
|
|
236
|
+
it("does not render content property as HTML", async () => {
|
|
237
|
+
await act(async () => {
|
|
238
|
+
SweetestAlert({
|
|
239
|
+
title: "Custom HTML",
|
|
240
|
+
content: "<strong>Test content</strong>",
|
|
241
|
+
confirm: mockConfirm,
|
|
242
|
+
cancel: mockCancel,
|
|
243
|
+
})
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
// Test that escaped HTML is displayed
|
|
247
|
+
expect(screen.getByText("<strong>Test content</strong>")).toBeInTheDocument()
|
|
248
|
+
|
|
249
|
+
// Test that actual HTML element is NOT created by checking there's no strong element
|
|
250
|
+
const strongElement = document.querySelector("strong")
|
|
251
|
+
expect(strongElement).not.toBeInTheDocument()
|
|
252
|
+
})
|
|
253
|
+
})
|
|
254
|
+
})
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import '@testing-library/jest-dom'
|
|
2
|
+
import { vi, beforeAll } from 'vitest'
|
|
3
|
+
|
|
4
|
+
// Mock dialog.showModal() method which is not available in jsdom
|
|
5
|
+
beforeAll(() => {
|
|
6
|
+
HTMLDialogElement.prototype.showModal = vi.fn(function (this: HTMLDialogElement) {
|
|
7
|
+
this.open = true
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
HTMLDialogElement.prototype.close = vi.fn(function (this: HTMLDialogElement) {
|
|
11
|
+
this.open = false
|
|
12
|
+
})
|
|
13
|
+
})
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "es2024",
|
|
4
|
+
"module": "esnext",
|
|
5
|
+
"lib": ["es2024", "DOM", "DOM.Iterable"],
|
|
6
|
+
"jsx": "react-jsx",
|
|
7
|
+
"moduleResolution": "bundler",
|
|
8
|
+
"resolveJsonModule": true,
|
|
9
|
+
"allowImportingTsExtensions": true,
|
|
10
|
+
"allowJs": true,
|
|
11
|
+
"checkJs": false,
|
|
12
|
+
"strict": true,
|
|
13
|
+
"noUnusedLocals": true,
|
|
14
|
+
"noUnusedParameters": true,
|
|
15
|
+
"noFallthroughCasesInSwitch": true,
|
|
16
|
+
"noEmit": true,
|
|
17
|
+
"esModuleInterop": true,
|
|
18
|
+
"skipLibCheck": true,
|
|
19
|
+
"forceConsistentCasingInFileNames": true
|
|
20
|
+
},
|
|
21
|
+
"include": ["src/**/*", "tests/sweetest_alert.test.jsx", "tests/test-setup.ts"],
|
|
22
|
+
"exclude": ["node_modules"]
|
|
23
|
+
}
|
package/vite.config.ts
ADDED
package/vitest.config.ts
ADDED