What this is
@playwright-labs/reporter-slack is a Playwright Reporter implementation that sends formatted Slack messages when a test run ends. It handles structuring results by status, surfacing error details inline, masking sensitive environment variables, and posting via either Incoming Webhooks or the Slack Web API.
This post covers the full API.
Installation
pnpm add -D @playwright-labs/reporter-slack @playwright-labs/slack-buildkit
slack-buildkit is a peer dependency used internally and also available for writing custom templates. If you only use built-in templates, you still need it installed.
Basic configuration
// 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,
}],
],
});
The reporter:
- Implements
onTestEnd(test, result) — accumulates [TestCase, TestResult][]
- Implements
onEnd(result) — resolves template(result, testCases) → SlackBlock[] → posts to Slack
The template option accepts a function, a SlackBlock[] array, a SlackMessage object, or an async function returning any of the above.
Three built-in templates
BaseTemplate
Minimal. Header, total/duration summary, list of failed tests (max 10 by default), optional report button.
import { BaseTemplate } from "@playwright-labs/reporter-slack/templates";
template: (result, testCases) =>
BaseTemplate(result, testCases, {
projectName: "My App",
reportUrl: process.env.REPORT_URL,
}),
Config:
type BaseTemplateOptions = {
projectName?: string; // default: "Playwright"
reportUrl?: string;
maxFailures?: number; // default: 10
};
WithOptionsTemplate
Status-grouped with an interactive static_select element. Groups are ordered: failed → timedOut → interrupted → skipped → passed. Filter options are built dynamically — if no tests timed out, no "Timed out" option appears.
import { WithOptionsTemplate } from "@playwright-labs/reporter-slack/templates";
template: (result, testCases) =>
WithOptionsTemplate(result, testCases, {
projectName: "My App",
reportUrl: process.env.REPORT_URL,
maxPerStatus: 5,
showTestNames: true,
show: {
passed: false, // omit passed group from the message
},
}),
Config:
type WithOptionsTemplateConfig = {
projectName?: string; // default: "Playwright"
reportUrl?: string;
maxPerStatus?: number; // default: 10
showTestNames?: boolean; // default: true
show?: {
failed?: boolean; // default: true
passed?: boolean; // default: true
skipped?: boolean; // default: true
timedOut?: boolean; // default: true
interrupted?: boolean; // default: true
};
};
Overflow behavior: when testCases.length > maxPerStatus, the section ends with _… and N more_.
Error display: for failed and timedOut tests, the first line of result.errors[0].message is rendered below the test name in italics.
WithTableTemplate
Same test summary as WithOptionsTemplate, but prepends a GFM table of environment variables (rendered via Slack's markdown block type).
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,
COMMIT_SHA: process.env.COMMIT_SHA,
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
OPTIONAL_VAR: process.env.OPTIONAL_VAR, // omitted if undefined
},
showRunSummary: true,
rowsPerChunk: 30,
}),
Config:
type WithTableTemplateConfig = {
projectName?: string; // default: "Playwright"
reportUrl?: string;
env: Record<string, string | undefined>; // required; undefined values omitted
tableTitle?: string; // default: "Environment"
mask?: boolean | string[]; // default: true
showRunSummary?: boolean; // default: true
rowsPerChunk?: number; // default: 30
};
rowsPerChunk controls how many rows per markdown block before the table is split. Slack's text limit per block is ~3000 characters; 30 rows is a safe conservative default.
Auto-masking
When mask: true (default), values are replaced with •••••••• for any key matching:
/token|secret|password|pass(?:word)?|credential|auth|api[_-]?key/i
Examples of keys that match: DB_PASSWORD, AUTH_TOKEN, API_KEY, SECRET_KEY, STRIPE_CREDENTIAL, OAUTH_CLIENT_SECRET.
Masking happens in the template before any data is serialized to JSON. The raw value never appears in the Slack payload.
Override options:
mask: ["CUSTOM_KEY", "INTERNAL_TOKEN"] // explicit list — only these keys
mask: false // no masking
Transport configuration
Incoming Webhook
send: { webhook: "https://hooks.slack.com/services/..." }
POSTs { blocks: SlackBlock[] } to the webhook URL. No OAuth, no scopes. Channel is fixed by the webhook app configuration.
Web API (chat.postMessage)
send: {
token: "xoxb-...", // bot token with chat:write scope
channel: "#ci-reports",
}
POSTs to https://slack.com/api/chat.postMessage with Authorization: Bearer <token>. Supports all Slack API features including thread replies and channel selection at post time.
Writing a custom template
A template is a function:
type SlackTemplate = (
result: FullResult,
testCases: [TestCase, TestResult][],
) => SlackBlock[] | SlackMessage | Promise<SlackBlock[] | SlackMessage>;
You can return raw SlackBlock[] JSON, or use the JSX primitives from @playwright-labs/slack-buildkit/react:
/** @jsxImportSource @playwright-labs/slack-buildkit/react */
import {
Actions, Blocks, Button, Context, Divider,
Header, Section, Table, Td, Th, Tr,
} from "@playwright-labs/slack-buildkit/react";
import type { FullResult, TestCase, TestResult } from "@playwright/test/reporter";
export function MyTemplate(
result: FullResult,
testCases: [TestCase, TestResult][],
) {
const failed = testCases.filter(([, r]) => r.status === "failed");
const passed = testCases.filter(([, r]) => r.status === "passed");
const emoji = result.status === "passed" ? ":white_check_mark:" : ":x:";
return (
<Blocks>
<Header>{`${emoji} My App — ${result.status === "passed" ? "All clear" : "Tests failed"}`}</Header>
<Section>{`*Total:* ${testCases.length} • ✅ ${passed.length} • ❌ ${failed.length}`}</Section>
{failed.length > 0 && (
<>
<Divider />
<Table>
<Tr><Th>Test</Th><Th>First error</Th></Tr>
{failed.map(([test, r]) => (
<Tr>
<Td>{`\`${test.titlePath().slice(1).join(" › ")}\``}</Td>
<Td>{r.errors[0]?.message?.split("\n")[0] ?? "—"}</Td>
</Tr>
))}
</Table>
</>
)}
<Divider />
<Actions>
<Button url={process.env.REPORT_URL!} style="primary" action_id="view_report">
View Full Report
</Button>
</Actions>
<Context>{`My App • ${new Date().toUTCString()}`}</Context>
</Blocks>
);
}
JSX runtime notes:
- Activated via
/** @jsxImportSource @playwright-labs/slack-buildkit/react */ or tsconfig.json
- No React, no DOM, no reconciler — purely synchronous function calls
<Table>/<Tr>/<Th>/<Td> converts to a { type: "markdown" } block with GFM table syntax
<Blocks> flattens all children into a flat SlackBlock[]
Register the template the same way as built-ins:
template: MyTemplate,
Testing templates
Templates return plain JSON arrays, so unit tests are straightforward:
import { test, expect } from "@playwright/test";
import type { FullResult } from "@playwright/test/reporter";
import { WithTableTemplate } from "@playwright-labs/reporter-slack/templates";
const failedRun: FullResult = {
status: "failed",
duration: 20000,
startTime: new Date("2025-04-28T10:00:00Z"),
};
test("sensitive values are masked", () => {
const blocks = WithTableTemplate(failedRun, [], {
env: { DB_PASSWORD: "supersecret", BRANCH: "main" },
});
const text = blocks
.filter((b) => b.type === "markdown")
.map((b) => (b as any).text)
.join("\n");
expect(text).not.toContain("supersecret");
expect(text).toContain("••••••••");
});
test("undefined env values are omitted from table", () => {
const blocks = WithTableTemplate(failedRun, [], {
env: { PRESENT: "yes", ABSENT: undefined },
});
const text = blocks
.filter((b) => b.type === "markdown")
.map((b) => (b as any).text)
.join("\n");
expect(text).toContain("`PRESENT`");
expect(text).not.toContain("`ABSENT`");
});
test("actions block absent when reportUrl is not set", () => {
const blocks = WithTableTemplate(failedRun, [], { env: {} });
expect(blocks.find((b) => b.type === "actions")).toBeUndefined();
});
test("actions block present when reportUrl is set", () => {
const blocks = WithTableTemplate(failedRun, [], {
env: {},
reportUrl: "https://ci.example.com",
});
expect(blocks.find((b) => b.type === "actions")).toBeDefined();
});
The test suite uses Playwright's runner — no additional test framework needed.
Source
github.com/vitaliharadkou/playwright-labs