DEV Community

Cover image for Stop Writing Custom Slack Notifications for Playwright — Use a Reporter
Vitali Haradkou
Vitali Haradkou

Posted on

Stop Writing Custom Slack Notifications for Playwright — Use a Reporter

Every CI pipeline eventually grows a Slack notification. Usually it starts as a one-liner: curl -X POST ... -d '{"text":"Tests failed"}'. Then someone wants the test count. Then the branch name. Then a link to the report. Then a list of failing tests. Before long you have three hundred lines of shell script and a bunch of JSON you're scared to touch.

@playwright-labs/reporter-slack is a Playwright reporter that handles all of that for you.


Setup in 30 seconds

pnpm add -D @playwright-labs/reporter-slack
Enter fullscreen mode Exit fullscreen mode
// playwright.config.ts
import { defineConfig } from "@playwright/test";
import { WithOptionsTemplate } from "@playwright-labs/reporter-slack/templates";

export default defineConfig({
  reporter: [
    ["@playwright-labs/reporter-slack", {
      send: { webhook: process.env.SLACK_WEBHOOK_URL },
      template: WithOptionsTemplate,
    }],
  ],
});
Enter fullscreen mode Exit fullscreen mode

That's it. When your test run finishes, Slack gets a message.


What the message looks like

The default WithOptionsTemplate groups tests by status and adds an interactive dropdown filter:

✅ My App — All tests passed

Total: 42   Duration: 18.4s   Started: Mon, 28 Apr 2025 ...

[Filter by status ▾]  [View Full Report →]

──────────────
✅ Passed — 40 tests
• `Auth › login works`
• `Auth › logout works`
• `Shop › add to cart`
…

⏩ Skipped — 2 tests
Enter fullscreen mode Exit fullscreen mode

If the run fails:

❌ My App — Tests failed

Total: 42   Duration: 22.1s

[Filter by status ▾]  [View Full Report →]

──────────────
❌ Failed — 3 tests
• `Auth › forgot password`
  _Expected button to be enabled_
• `Shop › checkout flow`
  _TimeoutError: locator('[data-testid=checkout]') timed out_
• `Shop › apply coupon`
  _AssertionError: expected 200, got 404_
Enter fullscreen mode Exit fullscreen mode

The first error line from each failed test appears directly under the test name — no clicking through to logs for the most common failures.


Three built-in templates

BaseTemplate — simple pass/fail

Good for small projects or when you just need signal in your channel. Header + test count + duration + list of failed tests + optional report button. Nothing else.

import { BaseTemplate } from "@playwright-labs/reporter-slack/templates";

reporter: [["@playwright-labs/reporter-slack", {
  send: { webhook: process.env.SLACK_WEBHOOK_URL },
  template: (result, testCases) =>
    BaseTemplate(result, testCases, {
      projectName: "My App",
      reportUrl: process.env.REPORT_URL,
    }),
}]]
Enter fullscreen mode Exit fullscreen mode

WithOptionsTemplate — status groups + interactive filter

Groups tests into status sections (failed → timedOut → interrupted → skipped → passed). Includes a static_select dropdown so anyone in the channel can filter by status without leaving Slack. Overflowing tests show an "…and N more" notice.

import { WithOptionsTemplate } from "@playwright-labs/reporter-slack/templates";

template: (result, testCases) =>
  WithOptionsTemplate(result, testCases, {
    projectName: "My App",
    reportUrl: process.env.REPORT_URL,
    maxPerStatus: 5,        // cap test names per group
    showTestNames: true,    // default true
    show: {
      passed: false,        // hide passed group (reduce noise on green runs)
    },
  }),
Enter fullscreen mode Exit fullscreen mode

WithTableTemplate — env context as a table

Same test summary, but adds a GFM table of environment variables below it. Useful when you run tests across multiple environments and need to know where the run happened.

import { WithTableTemplate } from "@playwright-labs/reporter-slack/templates";

template: (result, testCases) =>
  WithTableTemplate(result, testCases, {
    projectName: "My App",
    reportUrl: process.env.REPORT_URL,
    tableTitle: "Build Environment",
    env: {
      CI:         process.env.CI,
      BRANCH:     process.env.BRANCH,
      BUILD_ID:   process.env.BUILD_ID,
      DEPLOY_ENV: process.env.DEPLOY_ENV,
      DB_HOST:    process.env.DB_HOST,
      DB_PASSWORD: process.env.DB_PASSWORD,  // 👈 auto-masked
      API_KEY:    process.env.API_KEY,        // 👈 auto-masked
    },
  }),
Enter fullscreen mode Exit fullscreen mode

The rendered table looks like:

| Variable    | Value      |
| ---         | ---        |
| `CI`        | true       |
| `BRANCH`    | main       |
| `BUILD_ID`  | run-42     |
| `DB_HOST`   | db.internal|
| `DB_PASSWORD` | ••••••••  |
| `API_KEY`   | ••••••••   |
Enter fullscreen mode Exit fullscreen mode

Auto-masking sensitive variables

Any key whose name matches TOKEN, SECRET, PASSWORD, PASS, KEY, AUTH, or CREDENTIAL is automatically replaced with •••••••• before anything reaches the Slack API.

You can also be explicit:

mask: ["CUSTOM_SECRET", "INTERNAL_TOKEN"] // mask only these keys
mask: false                                // disable all masking
Enter fullscreen mode Exit fullscreen mode

Two transport options

Incoming Webhook — the simplest path. Create a webhook app in Slack, pass the URL:

send: { webhook: process.env.SLACK_WEBHOOK_URL }
Enter fullscreen mode Exit fullscreen mode

Web API — requires a Slack app with chat:write scope. More control: you can choose the channel, post as a bot, thread replies:

send: {
  token:   process.env.SLACK_BOT_TOKEN,
  channel: "#ci-reports",
}
Enter fullscreen mode Exit fullscreen mode

Writing your own template

All three templates are regular functions — (result: FullResult, testCases: SlackTestCases) => SlackBlock[]. You can write your own from scratch using the JSX components from @playwright-labs/slack-buildkit/react, or compose on top of an existing template.

/** @jsxImportSource @playwright-labs/slack-buildkit/react */
import { Blocks, Header, Section, Divider, Table, Tr, Th, Td, Context } from "@playwright-labs/slack-buildkit/react";
import type { FullResult } from "@playwright/test/reporter";
import type { SlackTestCases } from "@playwright-labs/reporter-slack";

export function MyTemplate(result: FullResult, testCases: SlackTestCases) {
  const failed = testCases.filter(([, r]) => r.status === "failed");
  const emoji = result.status === "passed" ? ":white_check_mark:" : ":x:";

  return (
    <Blocks>
      <Header>{`${emoji} My App — ${result.status}`}</Header>
      {failed.length > 0 && (
        <>
          <Divider />
          <Table>
            <Tr><Th>Test</Th><Th>Error</Th></Tr>
            {failed.map(([test, r]) => (
              <Tr>
                <Td>{test.title}</Td>
                <Td>{r.errors[0]?.message?.split("\n")[0] ?? ""}</Td>
              </Tr>
            ))}
          </Table>
        </>
      )}
      <Divider />
      <Context>{`Ran at ${new Date().toUTCString()}`}</Context>
    </Blocks>
  );
}
Enter fullscreen mode Exit fullscreen mode

The JSX here is not React — it's a custom runtime that outputs Slack Block Kit JSON directly. No DOM, no reconciler. The <Table> component converts its children to a GFM markdown block. Everything is synchronous and testable.


Testing your templates

Because templates return plain JSON arrays, assertions are trivial:

import { test, expect } from "@playwright/test";
import { WithTableTemplate } from "@playwright-labs/reporter-slack/templates";

test("masks DB_PASSWORD in table output", () => {
  const blocks = WithTableTemplate(failedRun, [], {
    env: { DB_PASSWORD: "supersecret", BRANCH: "main" },
  });
  const tableText = blocks
    .filter((b) => b.type === "markdown")
    .map((b) => (b as any).text)
    .join("\n");
  expect(tableText).not.toContain("supersecret");
  expect(tableText).toContain("••••••••");
  expect(tableText).toContain("main");
});
Enter fullscreen mode Exit fullscreen mode

Installation

pnpm add -D @playwright-labs/reporter-slack @playwright-labs/slack-buildkit
Enter fullscreen mode Exit fullscreen mode

If you've been hand-rolling Slack notifications from CI, give it a try. Feedback and issues welcome.

Top comments (0)