Popser

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:

AttributeValueAlways there
data-popser-root—Yes
data-popser-idToast IDYes
data-typesuccess | error | info | warning | loading | customYes
data-rich-colors—When richColors is on
data-unstyled—When unstyled is on
data-anchored—When anchored

Inner elements

AttributeElement
data-popser-viewportViewport container
data-popser-contentContent wrapper
data-popser-headerHeader row
data-popser-titleTitle text
data-popser-descriptionDescription text
data-popser-iconIcon wrapper
data-popser-actionsAction button container
data-popser-actionAction button
data-popser-cancelCancel button
data-popser-closeClose button
data-popser-arrowAnchor 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();
});

On this page