All-in-one assertion library for Playwright, Cypress, Jest, Vitest & WebdriverIO Fuzzy matching · Schema validation · Accessibility · AI-powered assertions
🌐 www.skakarh.com | 🏢 QAPulse-by-SK | ⭐ Star if it helped!
npm install qapulsesk-assertEvery framework has its own assertion API. Playwright has one. Cypress has another. Jest has another. When you switch projects, you relearn assertions.
qapulsesk-assert gives you one API that works everywhere.
Plus it adds assertions that simply don't exist anywhere:
| Problem | Standard Library | qapulsesk-assert |
|---|---|---|
| Text changed slightly | ❌ Test breaks | ✅ Fuzzy match handles it |
| API schema validation | ❌ 6 separate typeof checks |
✅ One assertSchema() call |
| Page load SLA | ❌ Manual math + throw | ✅ assertResponseTime() |
| Accessibility check | ❌ Needs axe-core setup | ✅ Zero-config toBeAccessible() |
| AI semantic assertions | ❌ Doesn't exist | ✅ toMean(), satisfiesRule() |
| Cross-framework | ❌ Framework-locked | ✅ Works everywhere |
import { qaPulseAssert, assertFuzzyMatch, assertSchema, assertResponseTime } from "qapulsesk-assert";
test("login page", async ({ page }) => {
await page.goto("/login");
const qa = qaPulseAssert(page);
// Fuzzy text match — handles "Login Page" / "Log In Page" / "LOGIN PAGE"
await qa.toFuzzyHaveText("h2", "Login Page", { threshold: 0.8 });
// Zero-config accessibility check
await qa.toBeAccessible();
});
test("API response", async ({ request }) => {
const res = await request.get("/api/posts/1");
const body = await res.json();
// Schema validation — all fields in one call
assertSchema(
{ status: res.status(), headers: {}, body },
{ id: "number", title: "string", body: "string", userId: "number" }
);
});const { assertFuzzyMatch, assertSchema, assertResponseTime } = require("qapulsesk-assert");
it("API schema validation", () => {
cy.request("/api/posts/1").then((res) => {
const result = assertSchema(
{ status: res.status, headers: {}, body: res.body },
{ id: "number", title: "string", userId: "number" }
);
expect(result.passed).to.be.true;
});
});import { assertFuzzyMatch, assertObjectContains, assertApproximately } from "qapulsesk-assert";
test("fuzzy match", () => {
const result = assertFuzzyMatch("Welcom to the internet", "Welcome to the internet", { threshold: 0.8 });
expect(result.passed).toBe(true);
console.log(result.message); // "Fuzzy match passed (similarity: 95.7%)"
});const qa = qaPulseAssert(page);
// Optional: AI-enhanced mode
const qa = qaPulseAssert(page, { ai: { enabled: true, apiKey: process.env.ANTHROPIC_API_KEY } });| Method | Description |
|---|---|
qa.toBeVisible(selector) |
Assert element is visible |
qa.toContainText(selector, text) |
Assert element contains text |
qa.toFuzzyHaveText(selector, text, opts?) |
Fuzzy text match with similarity threshold |
qa.toBeAccessible(opts?) |
Zero-config WCAG 2.1 accessibility check |
qa.toMean(selector, meaning, opts?) |
AI: does this element MEAN what you think? (AI key required) |
qa.satisfiesRule(selector, rule, opts?) |
AI: does this element satisfy a business rule? (AI key required) |
qa.pageMatchesSpec(spec, opts?) |
AI: does the page match this specification? (AI key required) |
import { assertFuzzyMatch, assertContains, assertMatches } from "qapulsesk-assert";Levenshtein distance-based similarity check. Returns { passed, message }.
const result = assertFuzzyMatch("Welcom to the internet", "Welcome to the internet", { threshold: 0.8 });
// result.passed → true
// result.message → "Fuzzy match passed (similarity: 95.7%)"| Option | Default | Description |
|---|---|---|
threshold |
0.8 |
Minimum similarity 0–1 |
caseSensitive |
false |
Case-sensitive matching |
Case-insensitive substring check.
const result = assertContains("Welcome to QA Pulse", "qa pulse");
// result.passed → trueRegex pattern validation.
const result = assertMatches("user@example.com", /^[\w.-]+@[\w.-]+\.\w+$/);
// result.passed → trueValue within ±tolerance range.
const result = assertApproximately(99.87, 100, 0.5);
// result.passed → true
// result.message → "Value 99.87 is within ±0.5 of 100"Array membership with deep equality.
const result = assertArrayContains([1, 2, 3], 2);
// result.passed → truePartial object matching — ignores extra properties.
const result = assertObjectContains(
{ id: 1, title: "QA Pulse", status: "active", createdAt: "2024-01-01" },
{ title: "QA Pulse", status: "active" } // only check these 2 fields
);
// result.passed → true
// result.message → "Object contains all expected properties"
// On failure:
// result.message → "Object mismatch:\n title: expected 'Wrong', got 'QA Pulse'"import { assertStatus, assertSuccess, assertBodyContains, assertSchema, assertResponseTime } from "qapulsesk-assert";All API assertions accept a response object: { status, headers, body, duration? }
assertStatus({ status: 200, headers: {}, body }, 200);
// "Response status is 200"Passes for any 2xx status code.
assertSuccess({ status: 201, headers: {}, body });
// "Response is successful (201)"Partial body matching.
assertBodyContains({ status: 200, headers: {}, body }, { id: 1, userId: 1 });
// "Response body contains all expected values"
// On failure: "Response body mismatch:\n 'id': expected 999, got 1"Type-based schema validation. Validates all fields in one call.
assertSchema(
{ status: 200, headers: {}, body },
{ id: "number", title: "string", userId: "number", body: "string" }
);
// "Response schema is valid"
// On failure: "Schema validation failed:\n 'title': expected number but got string"Performance SLA assertion.
assertResponseTime(
{ status: 200, headers: {}, body, duration: 1243 },
5000
);
// "Response time 1243ms is within 5000ms limit"
// On failure: "Response time 6100ms exceeds 5000ms limit"These use the Anthropic Claude API to understand what elements mean, not just what they contain.
const qa = qaPulseAssert(page, {
ai: {
enabled: true,
apiKey: process.env.ANTHROPIC_API_KEY
}
});
// Does the error message mean "authentication failed"?
await qa.toMean(".flash-error", "authentication failed");
// Does the page content match this specification?
await qa.pageMatchesSpec("The page should show a list of customer contacts with email addresses");
// Does this element satisfy a business rule?
await qa.satisfiesRule(".price-display", "must show a positive monetary value with currency symbol");See docs/QAPULSESK-ASSERT-COMPARISON.md in the Playwright boilerplate for a full honest comparison.
Summary:
// ❌ Standard Playwright — breaks if title separator changes
await expect(page).toHaveTitle("Accounts » SuiteCRM Demo");
// ✅ qapulsesk-assert — survives upgrades and format changes
await qa.toFuzzyHaveText("title", "Accounts SuiteCRM", { threshold: 0.6 });
// ❌ Standard — 4 separate type checks
expect(typeof body.id).toBe("number");
expect(typeof body.title).toBe("string");
expect(typeof body.userId).toBe("number");
expect(typeof body.body).toBe("string");
// ✅ qapulsesk-assert — one schema call with detailed error
assertSchema(response, { id: "number", title: "string", userId: "number", body: "string" });This is where qapulsesk-assert does something no other assertion library can do.
Standard assertions check what an element contains. AI assertions check what an element means.
// Standard assertion — passes for BOTH of these messages:
await expect(page.locator("#flash")).toContainText("Your account");
// "Your account has been created successfully!" ✅ correct
// "Your account has been suspended." ❌ wrong — but test PASSESStandard assertions are string-based. They have no concept of meaning, intent, or context. A message that contains "Your account" passes regardless of whether it is good news or bad news.
const qa = qaPulseAssert(page, {
ai: { enabled: true, apiKey: process.env.ANTHROPIC_API_KEY }
});
// AI checks semantic meaning — not exact text
await qa.toMean("#flash", "account was created successfully");
// ✅ PASSES for "Your account has been created!"
// ❌ FAILS for "Your account has been suspended."
// ❌ FAILS for "Your account was deleted."The AI reads the element, understands its content in context, and verifies that it conveys the intended meaning — regardless of exact wording, phrasing, or sentence structure.
Tests whether an element semantically means what you describe.
// Login success
await qa.toMean("#flash", "login was successful");
// Authentication failure
await qa.toMean("#flash", "authentication failed due to invalid credentials");
// CRM dashboard — user is logged in
await qa.toMean("title", "user is logged in to the CRM");
// Page heading purpose
await qa.toMean("h2", "this is a login or authentication page");Real-world value:
- Survives copy changes — "Login successful" → "Welcome back!" still passes
- Catches semantic bugs — wrong message type is caught even if text looks similar
- Language independent — works across English, Arabic, French without changes
Tests whether an element satisfies a business rule expressed in plain English.
// Business rule: nav links must be descriptive
await qa.satisfiesRule(
"ul li a",
"all links have descriptive text that clearly indicates their destination"
);
// Business rule: login form must have required security elements
await qa.satisfiesRule(
"form",
"contains a username field, a password field, and a submit button"
);
// Business rule: CRM Contact form must capture required fields
await qa.satisfiesRule(
"form",
"has fields for first name, last name, email address, and phone number"
);
// Business rule: error messages must not expose system internals
await qa.satisfiesRule(
"#flash",
"is a user-friendly error message that does not expose technical details or system internals"
);Real-world value:
- Write requirements as tests — turn acceptance criteria directly into assertions
- Catches UX violations — "click here" links, missing fields, exposed stack traces
- No selector archaeology — describe what you need, not how the DOM is structured
Tests whether the entire page matches a specification written in plain English. The most powerful of the three — describe what the page should do and Claude verifies it.
// Login page specification
await qa.pageMatchesSpec(
`This page should:
- Be a login or sign-in page
- Have a form with username and password fields
- Have a submit button to authenticate
- Not show any sensitive system information`
);
// SuiteCRM dashboard specification
await qa.pageMatchesSpec(
`This page should:
- Be a CRM dashboard or home page
- Show a logged-in user interface (not a login form)
- Have navigation to CRM modules like Contacts, Accounts, or Opportunities
- Display business data or dashboards`
);
// Contacts list specification
await qa.pageMatchesSpec(
`This page should:
- Show a list or grid of contacts
- Have column headers for contact information (name, email, phone)
- Allow users to search or filter contacts
- Be part of a CRM or contact management system`
);Real-world value:
- Replaces entire test suites for page-level validation
- Maps directly to acceptance criteria in Jira/Confluence
- Catches layout/content regressions that element-level tests miss
// Install
npm install qapulsesk-assert
// Set your Anthropic API key
// Option 1: .env file
ANTHROPIC_API_KEY=sk-ant-...
// Option 2: environment variable
export ANTHROPIC_API_KEY=sk-ant-...
// Option 3: inline (not recommended for production)
const qa = qaPulseAssert(page, {
ai: {
enabled: true,
apiKey: "sk-ant-...",
model: "claude-3-haiku-20240307" // optional — haiku is fast and cheap
}
});The with-packages branch of the Playwright boilerplate has 14 AI assertion tests — skipped by default, runnable with an API key:
# Clone the boilerplate
git clone -b with-packages https://github.com/QAPulse-by-SK/playwright-boilerplate.git
cd playwright-boilerplate && npm install && npx playwright install
# Run AI tests (requires Anthropic API key)
ANTHROPIC_API_KEY=your-key npx playwright test tests/packages/assert.ai.spec.ts
# Run all package tests (AI tests auto-skipped without key)
npx playwright test tests/packages/ --project=chromiumWhat the 14 tests cover:
| Suite | Tests | Target | What it tests |
|---|---|---|---|
qa.toMean() |
5 | the-internet + SuiteCRM | Semantic meaning of messages and page titles |
qa.satisfiesRule() |
4 | the-internet + SuiteCRM | Business rules — form fields, link quality, error messages |
qa.pageMatchesSpec() |
4 | the-internet + SuiteCRM | Full page against plain-English specifications |
| AI vs Standard | 1 | the-internet | Side-by-side comparison showing where AI adds value |
Two real-world targets:
the-internet.herokuapp.com— clean HTML, easy to understand the conceptdemo.suiteondemand.com— real enterprise CRM, shows AI on production-grade UI
| Standard Playwright | qapulsesk-assert AI | |
|---|---|---|
| Checks exact text | ✅ | ✅ |
| Handles copy changes | ❌ | ✅ |
| Understands meaning | ❌ | ✅ |
| Business rules | ❌ | ✅ |
| Full page spec | ❌ | ✅ |
| Language independent | ❌ | ✅ |
| API key required | ❌ | ✅ |
| Cost per assertion | Free | ~$0.001 (haiku) |
These test files in the QAPulse boilerplates use qapulsesk-assert against real-world sites:
| File | Tests | Target | Notes |
|---|---|---|---|
| assert.spec.ts | 23 | jsonplaceholder + the-internet | Full assertion suite |
| assert.demo.spec.ts | 19 | the-internet + SuiteCRM | Before/after storytelling |
| assert.ai.spec.ts | 14 | the-internet + SuiteCRM | AI features — skipped without API key |
| suitecrm.spec.ts | 26 | SuiteCRM enterprise CRM | Real-world CRM testing |
| assert.demo.cy.js | 15 | the-internet + SuiteCRM | Cypress version |
| suitecrm.cy.js | 23 | Cypress + SuiteCRM | Cypress CRM testing |
| Package | Description |
|---|---|
| qapulsesk-report | Dark-theme HTML reports, AI failure analysis, Slack webhooks |
| qapulsesk-gen | HAR → tests, recordings → tests, plain English → tests |
MIT © QA Pulse by SK
Built with ❤️ by QA Pulse by SK
🌐 skakarh.com · 🏢 QAPulse-by-SK · ⭐ Star if it helped!
| 🌐 Website | www.skakarh.com |
| 📦 All Open Source Products | skakarh.com/products |
| ✍️ QA Automation Blog | skakarh.com/blog |
| 🛠️ QA Consulting Services | skakarh.com/services |
| 🏢 GitHub Organisation | github.com/QAPulse-by-SK |
| 🎎 Playwright Boilerplate | github.com/QAPulse-by-SK/playwright-boilerplate |
| 🌲 Cypress Boilerplate | github.com/QAPulse-by-SK/cypress-boilerplate |
| 🐍 Selenium Boilerplate | github.com/QAPulse-by-SK/selenium-boilerplate |
| 📊 qapulsesk-report | npmjs.com/package/qapulsesk-report |
| 🤖 qapulsesk-gen | npmjs.com/package/qapulsesk-gen |
