惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Spread Privacy
Spread Privacy
Engineering at Meta
Engineering at Meta
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
D
DataBreaches.Net
N
Netflix TechBlog - Medium
T
The Blog of Author Tim Ferriss
L
LangChain Blog
Jina AI
Jina AI
MongoDB | Blog
MongoDB | Blog
B
Blog
酷 壳 – CoolShell
酷 壳 – CoolShell
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
月光博客
月光博客
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
T
Tailwind CSS Blog
Google DeepMind News
Google DeepMind News
WordPress大学
WordPress大学
T
Threatpost
V
V2EX
Security Archives - TechRepublic
Security Archives - TechRepublic
GbyAI
GbyAI
Scott Helme
Scott Helme
Cyberwarzone
Cyberwarzone
H
Help Net Security
大猫的无限游戏
大猫的无限游戏
Security Latest
Security Latest
T
The Exploit Database - CXSecurity.com
T
Tenable Blog
S
Schneier on Security
博客园 - 叶小钗
Apple Machine Learning Research
Apple Machine Learning Research
F
Full Disclosure
腾讯CDC
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
S
Security @ Cisco Blogs
A
Arctic Wolf
S
Securelist
Recent Commits to openclaw:main
Recent Commits to openclaw:main
T
Tor Project blog
The Register - Security
The Register - Security
L
LINUX DO - 最新话题
Blog — PlanetScale
Blog — PlanetScale
U
Unit 42
V
Vulnerabilities – Threatpost
Google Online Security Blog
Google Online Security Blog
L
LINUX DO - 热门话题
TaoSecurity Blog
TaoSecurity Blog
Y
Y Combinator Blog
博客园 - 三生石上(FineUI控件)
C
CERT Recently Published Vulnerability Notes

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
Automated Accessibility Testing: axe-core, Keyboard Navigation, and WCAG in the Browser
Kevin Julián Martínez Escobar · 2026-06-23 · via DEV Community

Automated accessibility testing usually means one thing: point a scanner at the page, let it check the initial HTML against a set of WCAG rules, and turn green. Missing alt text, an unlabeled input, low-contrast text at load. Caught. Done.

Then a user opens the menu, submits the empty form, expands the accordion, and lands in a state your scanner never saw. The newly revealed panel steals focus and never gives it back. The modal traps a keyboard user with no way out. None of that exists in the first render, so none of it was tested.

That is the gap this post is about. Accessibility is not a property of a page, it is a property of every state a page can reach, and most of those states only exist after someone interacts. The fix is to run your accessibility checks in a real browser, after the interaction, with two complementary tools: axe-core for WCAG rule checks, and keyboard navigation tests for focus order. This walks through both.

The gap: accessibility tests that run in jsdom

The default setup runs your tests in jsdom. Render a component with Testing Library, run axe-core over it with jest-axe or vitest-axe, assert no violations. It is fast, and it is fine for catching the obvious stuff: a button with no accessible name, an input with no label, a misused ARIA attribute.

It also has a hard ceiling. jsdom does no layout and no rendering, so any check that depends on what actually paints on screen cannot run there. axe-core's color-contrast rule is the clearest example: in jsdom it comes back as "incomplete," not pass or fail, because there is nothing to measure. And that is before you get to anything interactive.

What it cannot tell you:

  • Whether focus moves somewhere sensible after a dialog opens, and comes back when it closes.
  • Whether the validation summary that renders after a failed submit is actually announced and reachable.
  • Whether your tab order matches the visual order once a section expands.
  • Whether a custom component announces its name, role, and state to a screen reader after it changes.

These are not edge cases. They are where most real accessibility complaints come from. And they all depend on layout, focus, and event behavior that jsdom only approximates. To test them you need the real thing: a real browser, real focus, real events.

That is the part TWD is built for. Tests run inside the actual browser, against your real app, and you drive them the way a user would. Which means you can scan accessibility after the interaction, not just before it.

Approach 1: axe-core, scanned after the interaction

axe-core is the engine behind most accessibility tooling. It is excellent at the rule-based checks: contrast, names, roles, ARIA misuse. The trick is not running axe. The trick is running it against the state that matters.

A small helper turns axe results into a readable failure instead of a wall of JSON:

import axe from "axe-core";

type AxeResults = import("axe-core").AxeResults;
type RunOptions = import("axe-core").RunOptions;
type ElementContext = import("axe-core").ElementContext;

/**
 * Run axe-core against the page (or a scoped element) and throw a
 * formatted error if there are any accessibility violations.
 */
export async function checkA11y(
  context?: ElementContext,
  options?: RunOptions
): Promise<AxeResults> {
  const results = await axe.run(context ?? document, options);

  if (results.violations.length) {
    const report = results.violations
      .map((v, i) => `${i + 1}. [${v.impact ?? "n/a"}] ${v.id}: ${v.help}\n   ${v.helpUrl}`)
      .join("\n\n");
    throw new Error(
      `Found ${results.violations.length} accessibility violation(s):\n\n${report}`
    );
  }

  return results;
}

A baseline scan of the page that loads is the easy case:

import { twd, userEvent, screenDom } from "twd-js";
import { describe, it, beforeEach } from "twd-js/runner";
import { checkA11y } from "./helpers/axe";

// Only WCAG 2.0/2.1 A & AA rules. Skips axe "best-practice" rules
// (region, landmark-one-main, ...) which are recommendations, not failures.
const WCAG = {
  runOnly: {
    type: "tag" as const,
    values: ["wcag2a", "wcag2aa", "wcag21a", "wcag21aa"],
  },
};

describe("Accessibility", () => {
  beforeEach(() => {
    twd.clearRequestMockRules();
  });

  it("should have no WCAG 2 A/AA violations on the home page", async () => {
    await twd.visit("/");
    await checkA11y(document, WCAG);
  });
});

That is the part every tool can do. Here is the part that needs a real browser and a real interaction: scanning a state that only exists after the user does something.

it("should stay accessible after form validation errors render", async () => {
  await twd.mockRequest("getTodoList", {
    method: "GET",
    url: "/api/todos",
    response: [],
    status: 200,
  });
  await twd.visit("/todos");
  await twd.waitForRequest("getTodoList");

  // Submit the empty form to force the validation error messages to render.
  const submitButton = await screenDom.findByRole("button", { name: "Create Todo" });
  await userEvent.click(submitButton);

  // Confirm the new dynamic content actually appeared before scanning.
  const error = await screenDom.findByText("Title is required");
  twd.should(error, "be.visible");

  // Scope the scan to just the form so the report is about this component.
  const form = await screenDom.findByTestId("todo-form");
  await checkA11y(form, WCAG);
});

The error messages in that test do not exist in the initial HTML. They render after a failed submit. A static scan never reaches them, so it never checks whether they are associated with their fields, announced, or reachable. Driving the interaction first, then scoping the scan to the form, checks the state a user actually lands in.

Scoping matters too. Passing the form element instead of document keeps the report about the component you just exercised, rather than re-flagging everything else on the page.

Approach 2: keyboard navigation and focus order

axe will not tell you whether your tab order makes sense. It cannot. Whether Tab moves through controls in an order that matches the visual flow, whether focus is visible, whether a control is even reachable by keyboard: that is behavior, and you have to drive it.

Because TWD tests run in the real browser, userEvent drives real focus. So you can walk the page the way a keyboard user does and assert where focus lands at each step.

it("keyboard navigation should work", async () => {
  await twd.visit("/chat");

  const user = userEvent.setup();

  // Tab from the top of the page. First stop should be the back link.
  await user.tab();
  const backToLanding = screenDom.getByRole("link", { name: "← Back to Landing" });
  twd.should(backToLanding, "be.focused");

  // Tab again. Focus should land on the prompt input, and typing should work.
  await user.tab();
  await user.keyboard("write some text");
  const textarea = screenDom.getByLabelText("Main prompt input (required)");
  twd.should(textarea, "have.text", "write some text");
});

This is a deterministic check of two things a scanner cannot see: the focus order is correct (back link, then input), and the input is reachable and usable by keyboard alone. If someone later drops a non-focusable div with a click handler into the flow, or reorders the DOM so tab order no longer matches the layout, this test fails. You find out in the run, not from a user report.

You can extend the same pattern to the cases that bite hardest: open a dialog and assert focus moved into it, close it and assert focus returned to the trigger, tab to the end of a menu and assert it does not escape into the page behind it.

Watch out with automated accessibility testing

Automated accessibility testing has a ceiling. Rule-based tools catch roughly a third of WCAG issues. axe is very good inside that third, and driving the interaction first stretches it to states a load-time scan cannot reach, but it is still a third. The rest needs a human: real screen reader testing, and judgment about whether labels, content order, and motion actually make sense.

So treat this as the regression net, not the whole strategy. It is the part you can run on every change, in CI, that stops obvious and post-interaction failures from sliding back in between manual audits.

Why this matters to us

We build the tool this runs on, twd-js, and we treat its own accessibility as a first-class requirement, not a nice-to-have. A tool whose entire job is to help people ship correct software has no business locking anyone out of using it. So the sidebar UI is fully keyboard navigable with a logical focus order and no traps, the focus indicator is visible in both light and dark themes, and text and controls meet WCAG contrast ratios. We had it audited by an external accessibility consultancy against WCAG 2.2 Level AA, and it came back at full conformance. You can read the accessibility statement for the full scope and methodology.

Try it

Both approaches are just tests, running in your real app, in a real browser:

npm install twd-js

Point a scan at the page that loads if you want a baseline. Then write the one that clicks the button first. That second test is the one your users were waiting for.