Testing
Stable selectors. data-popser-id on everything. Zero excuses for flaky tests.
Every toast renders data-popser-id on its root. Stable selectors for Playwright, Cypress, or whatever testing framework you've reluctantly adopted.
Data attributes
Toast root
<div
data-popser-root
data-popser-id="my-toast-id"
data-type="success"
>
...
</div>Every toast root has:
| Attribute | Value | Always there |
|---|---|---|
data-popser-root | — | Yes |
data-popser-id | Toast ID | Yes |
data-type | success | error | info | warning | loading | custom | Yes |
data-rich-colors | — | When richColors is on |
data-unstyled | — | When unstyled is on |
data-anchored | — | When anchored |
Inner elements
| Attribute | Element |
|---|---|
data-popser-viewport | Viewport container |
data-popser-content | Content wrapper |
data-popser-header | Header row |
data-popser-title | Title text |
data-popser-description | Description text |
data-popser-icon | Icon wrapper |
data-popser-actions | Action button container |
data-popser-action | Action button |
data-popser-cancel | Cancel button |
data-popser-close | Close button |
data-popser-arrow | Anchor arrow |
Playwright
// wait for a toast
await page.locator('[data-popser-id="my-toast"]').waitFor();
// check the text
await expect(
page.locator('[data-popser-root][data-type="success"] [data-popser-title]')
).toHaveText("Saved");
// click the action button
await page
.locator('[data-popser-id="upload-toast"] [data-popser-action]')
.click();
// close it
await page
.locator('[data-popser-id="my-toast"] [data-popser-close]')
.click();
// count them
const count = await page.locator('[data-popser-root]').count();
// wait for it to vanish
await page.locator('[data-popser-id="my-toast"]').waitFor({ state: "detached" });Cypress
// exists?
cy.get('[data-popser-id="my-toast"]').should("exist");
// right type?
cy.get('[data-popser-root][data-type="error"]').should("be.visible");
// right text?
cy.get('[data-popser-id="my-toast"] [data-popser-title]')
.should("have.text", "Something broke");
// click the action
cy.get('[data-popser-id="my-toast"] [data-popser-action]').click();
// gone?
cy.get('[data-popser-id="my-toast"]').should("not.exist");Custom IDs
Pass a custom id for predictable selectors. Auto-generated IDs are fine for humans, rubbish for tests.
toast.success("Saved", { id: "save-toast" });
toast.error("Failed", { id: "error-toast" });
toast.loading("Uploading...", { id: "upload-toast" });Then target with [data-popser-id="save-toast"] instead of guessing.
useToaster hook
For component-level assertions. Gives you the toast list:
import { useToaster } from "@vcui/popser";
function ToastDebugPanel() {
const { toasts } = useToaster();
return (
<div data-testid="toast-debug">
<span>{toasts.length} active toasts</span>
</div>
);
}Must be inside <Toaster> — that's where the Base UI context lives.
toast.getToasts()
Active toast IDs. No DOM required.
import { toast } from "@vcui/popser";
const ids = toast.getToasts();
// ["abc-123", "def-456"]Vitest
Popser uses Vitest with happy-dom internally. Testing components that call toast()? Either mock the module or render an actual <Toaster>:
import { render, screen } from "@testing-library/react";
import { toast, Toaster } from "@vcui/popser";
test("shows success toast", async () => {
render(<Toaster />);
toast.success("It works", { id: "test-toast" });
const toastEl = await screen.findByText("It works");
expect(toastEl).toBeInTheDocument();
});