今日已更新 339 条资讯 | 累计 19899 条内容
关于我们

标签:#playwright

找到 20 篇相关文章

AI 资讯

Diagnosing Cloudflare Blocks Before Changing Your Scraper

A scraper fails, someone swaps the User-Agent, someone else adds a proxy, then the job starts passing locally but fails again in CI. That usually happens because Cloudflare did not block “scraping” as one thing. It evaluated several signals, and each failure needs a different fix. This is about authorized automation: your own sites, customer-approved workflows, testing, monitoring, data access you are allowed to perform. If you do not have permission to automate against a site, changing fingerprints or rotating IPs does not make it okay. Start with the failure you actually see Cloudflare failures often get collapsed into “403”, but the page body matters. Common cases: Error 1020 : usually an access denied page from a Cloudflare rule or bot score decision. The HTTP status may still be 403, so inspect the HTML. 403 without a 1020 page : often IP reputation, firewall rules, geo restrictions, or an auth problem. 429 : rate limit exhaustion. Slowing down can help here, but it will not fix a fingerprint problem. Endless Just a moment... page : your client did not complete the browser-side challenge. CAPTCHA or Turnstile loop : Cloudflare still considers the session borderline after earlier checks. Add classification before you add workarounds. Even a basic classifier saves time: import time import requests CLOUDFLARE_MARKERS = { " 1020 " : " cloudflare_access_denied " , " Just a moment " : " cloudflare_js_challenge " , " cf-turnstile " : " cloudflare_turnstile " , " cf-error-code " : " cloudflare_error_page " , } def classify_response ( resp : requests . Response ) -> str : body = resp . text [: 5000 ] if resp . status_code == 429 : return " rate_limited " for marker , label in CLOUDFLARE_MARKERS . items (): if marker in body : return label if resp . status_code == 403 : return " forbidden_unknown " return " ok " if resp . ok else f " http_ { resp . status_code } " def get_with_backoff ( url : str , max_attempts = 4 ): for attempt in range ( max_attempts ): resp = request

2026-07-14 原文 →
AI 资讯

How One Log File Turned Into Five Context Switches

The issue was not the tools. It was opening five of them before deciding what the log file was for. The log file was already on the screen. A remote Windows workstation had failed a desktop build, and the relevant file was sitting in a local app directory, something like: C:\Users\<user>\AppData\Local\<app>\logs\build.log The remote session was working. The error was visible. The next step seemed small: get the log back to the local laptop, open it in a familiar editor, compare it with the issue notes, and pull out the part that mattered. That should have been a 30-second task. Instead, it turned into five context switches. The first context was the remote session The remote desktop session made sense. The build failed on that machine, the app was installed there, and the log path was easier to find visually than by guessing from memory. So far, nothing was wrong. The file was selected. The timestamp matched the failed run. The log looked useful. It probably had the stack trace, the missing dependency path, or the configuration mismatch that explained the build failure. Then came the small but surprisingly annoying question: How should this file leave the remote machine? That is where the workflow started to wobble. The second context was chat The first instinct in many teams is chat. Drop the file into a message to yourself, a teammate, or the debugging thread. It is fast, already open, and keeps the file near the conversation. For some files, that is the right move. A screenshot, a short error snippet, or a quick “does this look familiar?” artifact belongs naturally in the discussion. But a full log file is not always a chat artifact. If it goes into chat, will anyone know later whether it was the first failing run or the second? Will it be obvious which remote machine produced it? Will the file still be easy to find after the thread moves on? Chat was not wrong. It was just not clearly the right home for this specific file. So the workflow moved on. The third con

2026-07-09 原文 →
AI 资讯

Stop Manually Booking Appointments: Building an Autonomous AI Health Agent with Playwright and GPT-4o

We’ve all been there. You get a notification from your smartwatch saying your heart rate has been a bit funky, or your blood oxygen is dipping. Usually, we ignore it until it becomes a problem. But what if your personal AI was looking out for you? 🤖 In this tutorial, we are building an Autonomous Health Agent . This isn't just a notification bot; it's a proactive system that uses Playwright browser automation , OpenAI Function Calling , and Python to monitor your health trends and—if things look suspicious for three days straight—literally opens a browser and books a doctor's appointment for you. By leveraging Autonomous AI Agents and Playwright automation , we are moving from "Passive Monitoring" to "Active Intervention." This is the future of Health Tech Automation . 🏗 The Architecture Before we dive into the code, let's look at how the data flows from a "scary heart rate" to a "confirmed appointment." graph TD A[Wearable Data/Health Logs] --> B{3-Day Anomaly Check} B -- Normal --> C[Stay Healthy! 🟢] B -- Abnormal --> D[Trigger AI Agent 🤖] D --> E[OpenAI Function Calling] E --> F[Playwright Browser Automation] F --> G[Hospital Booking Platform] G --> H[Appointment Confirmation 🏥] H --> I[Notify User via SMS/Email] 🛠 Prerequisites To follow along, you’ll need: Python 3.10+ Playwright : The king of modern browser automation. OpenAI API Key : For the "brain" of our agent. A healthy dose of curiosity! 🥑 pip install playwright openai pydantic playwright install chromium 👨‍💻 Step 1: Defining the "Brain" (OpenAI Function Calling) We don't want the LLM to just "talk" about booking an appointment; we want it to actually execute the action. We'll use OpenAI's Function Calling to bridge the gap between text and code. import json from openai import OpenAI client = OpenAI () # Define the tool our agent can use tools = [ { " type " : " function " , " function " : { " name " : " book_doctor_appointment " , " description " : " Books a medical appointment based on department and s

2026-07-02 原文 →
AI 资讯

OTP Verification in Playwright Without Regex

Most guides to OTP testing in Playwright include a function that looks something like this: function extractOtp ( emailBody : string ): string { const patterns = [ / \b(\d{6})\b / , /code [ : \s] + (\d{4,8}) /i , /verification [ : \s] + (\d{4,8}) /i , /OTP [ : \s] + (\d{4,8}) /i , ]; for ( const pattern of patterns ) { const match = emailBody . match ( pattern ); if ( match ) return match [ 1 ]; } throw new Error ( ' OTP not found in email body ' ); } This function is fragile. It breaks when the email template changes. It returns false positives when the email body contains order IDs or timestamps. It requires you to maintain regex patterns for every email provider your app might use. There is a better way. The Problem with Regex OTP Extraction When your app sends a verification email, the OTP is buried somewhere in the HTML body. To extract it you need to: Fetch the raw email body Parse HTML or plain text Apply regex patterns that match your specific email format Handle edge cases — 4-digit vs 6-digit codes, codes in tables, codes in buttons Every time your email provider changes their template, your regex breaks. Every time you add a new auth provider, you write new patterns. It is maintenance overhead that compounds forever. The right place to extract the OTP is at the infrastructure layer — before the email even reaches your test suite. How ZeroDrop Extracts OTPs at the Edge ZeroDrop catches emails at Cloudflare's edge before storing them. When an email arrives, the worker runs OTP detection on the body and stores the result as a structured field alongside the raw email. By the time your test calls waitForLatest() , the OTP is already extracted and sitting in email.otp . No regex. No HTML parsing. No maintenance. const email = await mail . waitForLatest ( inbox ); email . otp // "847291" — already extracted Setup npm install zerodrop-client No API key. No signup. No environment variables. Basic OTP Test import { test , expect } from ' @playwright/test ' ; import

2026-06-27 原文 →
AI 资讯

3 Tests That Pass in LangFlow But Fail in n8n Production

You built a LangFlow prototype. Every test passed. You exported the flow, dropped it into n8n, and the first production run broke. This is not a bug report. It is a pattern. The three-year SDET who has built LangFlow prototypes but hit mysterious failures when deploying the same logic in n8n production already knows the feeling. The prototype felt solid. The production pipeline felt like a different language. It is not. The difference is execution context. LangFlow runs in a notebook-like environment where state is forgiving and retries are invisible. n8n runs in a workflow engine where every node is a transaction boundary and every failure is final unless you explicitly handle it. Here are the three tests that pass in LangFlow but fail in n8n production, and what they teach about building reliable AI pipelines. Test 1: The "LLM Returns Valid JSON" Test What passes in LangFlow: You send a prompt asking the model to return JSON. The response comes back as a string. You parse it with json.loads() . It works. You move on. What fails in n8n: The model returns a string that starts with a code block. Or a trailing comma. Or a markdown fence. Or a preamble sentence before the JSON. Or nothing at all because the context window was exceeded. Why the difference: LangFlow's Python node silently tolerates malformed output. If json.loads() fails, you see the error in the output panel and fix the prompt. n8n's JSON node does not retry. It does not fall back. It throws a structured error that stops the entire workflow. The fix is not a better prompt. The fix is a validation layer that normalizes LLM output before parsing. import json import re def extract_json ( raw : str ) -> dict : # Strip markdown fences cleaned = re . sub ( r ' ^``` (?:json)?\s* ' , '' , raw . strip ()) cleaned = re . sub ( r ' \s* ```$ ' , '' , cleaned ) # Find the first { and last } start = cleaned . find ( ' { ' ) end = cleaned . rfind ( ' } ' ) if start == - 1 or end == - 1 : raise ValueError ( " No JSON o

2026-06-23 原文 →
AI 资讯

The Playwright Playbook — Part 7: The CI/CD Setup Nobody Shows You

The Playwright Playbook — Part 7: The CI/CD Setup Nobody Shows You "A test suite that only runs on your laptop isn't a test suite. It's a hobby." Six parts in, we have a serious framework. POM-based UI tests. Network interception. Multi-user contexts. A full API testing layer. Visual regression across four viewports. A complete debugging toolkit. Now it needs to run automatically. On every pull request. On every merge. On every deployment. Without you touching it. Most CI/CD tutorials for Playwright show you this: # The "tutorial" version everyone copies - run : npx playwright test That's not a CI setup. That's a shell command in a YAML file. A real production CI/CD pipeline for Playwright has: Sharding — split tests across multiple machines and finish in a fraction of the time Browser matrix — Chromium, Firefox, WebKit in parallel Docker — identical environment on every machine, every time Artifacts — HTML report, traces, screenshots, videos — downloadable from every run Failure notifications — your team knows within seconds, not the next morning Separate VRT workflow — visual regression on its own cadence, not blocking every PR Environment-specific pipelines — staging vs production, different configurations Let's build all of it. 🎯 🏗️ Where We Left Off After Part 6, our full project structure is: playwright-playbook/ ├── tests/ │ ├── auth/login.spec.ts ✅ Part 1 │ ├── tasks/task-management.spec.ts ✅ Part 1 │ ├── network/ ✅ Part 2 │ ├── multi-user/ ✅ Part 3 │ ├── multi-tab/ ✅ Part 3 │ ├── api/ ✅ Part 4 │ ├── visual/ ✅ Part 5 │ └── debug/trace-examples.spec.ts ✅ Part 6 ├── pages/ │ ├── LoginPage.ts ✅ Part 1 │ ├── TaskPage.ts ✅ Part 1 │ └── DashboardPage.ts ✅ Part 3 ├── api/ │ ├── TaskApiClient.ts ✅ Part 4 │ └── AuthApiClient.ts ✅ Part 4 ├── fixtures/ │ ├── auth.fixture.ts ✅ Part 1 │ ├── tasks.json ✅ Part 2 │ ├── empty-tasks.json ✅ Part 2 │ ├── tasks-har.har ✅ Part 2 │ ├── multi-user.fixture.ts ✅ Part 3 │ └── api.fixture.ts ✅ Part 4 ├── scripts/ │ └── record-har.ts ✅

2026-06-21 原文 →
AI 资讯

The Tester Who Had 10 Certifications But Couldn't Write a Single Test That Caught a Bug

You have ISTQB Foundation. ISTQB Advanced. Certified ScrumMaster. A cloud cert. A security testing cert. Maybe a Python for Testers badge from a platform. And you still cannot write a test that finds a real bug. I interviewed someone like you last quarter. The resume was a wall of acronyms. The conversation was a wall of theory. "I follow the V-model." "I use equivalence partitioning." "I believe in shift-left." Then I asked: "Show me one test you wrote that caught something the developer missed." Silence. Not because they were nervous. Because they had never written a test that found a bug. They had written tests that passed. They had written tests that covered requirements. They had never written a test that broke something. That is the difference between a certification holder and a tester. Certifications test your memory. Bugs test your thinking. Let me show you what I mean. The Certification Trap Certifications are not useless. They give you vocabulary. They give you structure. They give you something to put on LinkedIn so recruiters stop asking if you know what a test case is. But they do not teach you how to find bugs. Here is why. Every certification exam tests known knowledge. You study a syllabus. You memorize definitions. You answer multiple-choice questions about boundary value analysis. You pass. Then you sit in front of an application. The application does not have a syllabus. It does not have a boundary value analysis section in the documentation. It has a login form that sometimes lets you in with a password that is clearly wrong, but only on Tuesdays, and only if the server clock is behind by exactly four minutes. No certification prepares you for that. The tester with 10 certifications treats testing like a checklist. They write test cases from requirements. They execute them. They mark pass or fail. They report coverage metrics. The tester who finds bugs treats testing like an investigation. They start with a hypothesis. They try to prove the appl

2026-06-20 原文 →
AI 资讯

The Playwright Playbook — Part 3: Multi-User, Multi-Tab & Browser Context Testing

The Playwright Playbook — Part 3: Multi-User, Multi-Tab & Browser Context Testing "Most frameworks test one user at a time. Real apps don't work that way." In Part 1, we built the full foundation — POM, storageState , fixtures, and a clean project structure. In Part 2, we took control of the network layer — mocking APIs, simulating failures, and asserting on real network calls. Now we tackle the scenario that breaks most automation frameworks. Multiple users. Simultaneously. In one test. Think about your app for a moment. How many features actually involve just one user? Admin assigns a task → regular user sees it appear in real time User A edits a record → User B gets a notification Admin revokes access → User's session should invalidate Two users try to edit the same record → conflict resolution kicks in These are real features. They need to be tested. And page.goto('/login') in a beforeEach won't get you there. Playwright's browser context architecture was built exactly for this. Let's use it properly. 🎯 🏗️ Where We Left Off After Part 2, our project looks like this: playwright-playbook/ ├── tests/ │ ├── auth/ │ │ └── login.spec.ts ✅ Part 1 │ ├── tasks/ │ │ └── task-management.spec.ts ✅ Part 1 │ └── network/ ✅ Part 2 │ ├── api-mocking.spec.ts │ ├── error-simulation.spec.ts │ └── network-assertions.spec.ts ├── pages/ │ ├── LoginPage.ts ✅ Part 1 │ └── TaskPage.ts ✅ Part 1 ├── fixtures/ │ ├── auth.fixture.ts ✅ Part 1 │ ├── tasks.json ✅ Part 2 │ ├── empty-tasks.json ✅ Part 2 │ └── tasks-har.har ✅ Part 2 ├── scripts/ │ └── record-har.ts ✅ Part 2 ├── .auth/ │ ├── admin.json │ └── user.json ├── global-setup.ts ✅ Part 1 ├── playwright.config.ts ✅ Part 1 └── .env By the end of Part 3, we add: playwright-playbook/ ├── tests/ │ ├── multi-user/ ← NEW │ │ ├── role-permissions.spec.ts │ │ └── realtime-collaboration.spec.ts │ └── multi-tab/ ← NEW │ └── multi-tab-flows.spec.ts ├── pages/ │ └── DashboardPage.ts ← NEW ├── fixtures/ │ └── multi-user.fixture.ts ← NEW Every one of th

2026-06-17 原文 →
AI 资讯

OTP Verification in Playwright Without Regex

Every developer who has written a Playwright test for OTP verification has written this line: const otp = email . body . match ( / \b\d{6}\b / )?.[ 0 ]; It works. Until it doesn't. The email body changes format. The OTP appears inside an HTML table. The sending service wraps it in a <span> . Your regex matches a phone number instead of the code. The test fails intermittently and you spend an hour debugging something that has nothing to do with the feature you're testing. The regex problem OTP extraction via regex is brittle by nature. You're pattern-matching against a string that your email sending service controls — not you. Any time the template changes, your tests break. Here's what a typical OTP test looks like today: import { test , expect } from ' @playwright/test ' ; import { ZeroDrop } from ' zerodrop-client ' ; const mail = new ZeroDrop (); test ( ' user can verify OTP ' , async ({ page }) => { const inbox = mail . generateInbox (); // 1. Trigger OTP send await page . goto ( ' /login ' ); await page . fill ( ' [data-testid="email"] ' , inbox ); await page . click ( ' [data-testid="submit"] ' ); // 2. Wait for email const email = await mail . waitForLatest ( inbox , { timeout : 15000 }); // 3. Extract OTP — the fragile part const otp = email . body . match ( / \b\d{6}\b / )?.[ 0 ]; if ( ! otp ) throw new Error ( ' OTP not found in email body ' ); // 4. Enter OTP await page . fill ( ' [data-testid="otp"] ' , otp ); await page . click ( ' [data-testid="verify"] ' ); await expect ( page ). toHaveURL ( ' /dashboard ' ); }); The test works — but line 14 is carrying all the risk. Change the email template and the test breaks. Add a phone number to the footer and the regex matches the wrong number. Send a 4-digit OTP instead of 6 and you need to update the pattern. OTP extraction at the edge ZeroDrop extracts OTPs before they reach your test. The Cloudflare Worker that catches incoming emails runs a pattern match on the plain-text body and stores the result alongsi

2026-06-15 原文 →
AI 资讯

How to Automate Publishing to CSDN and WeChat MP Using Playwright (When APIs Fail)

Overview Today's focus was on automating article publishing to CSDN and WeChat MP (微信公众号) using Playwright, after CSDN deprecated its public Open API. Key achievements include: injecting Markdown content into CSDN's dynamic editor, handling title input quirks, implementing QR code login for WeChat MP, updating the Dev.to API publisher, and consolidating platform configs into a single YAML file. We also fixed session log capture after a Claude Code update changed the log file path. Problems and Solutions 1. CSDN Open API Deprecation → Browser Automation Background : In early 2026, CSDN silently shut down its public Open API. All endpoints returned 404/403. We needed a fallback to keep publishing to China's largest developer platform. Solution : Use Playwright to simulate a real user login and article creation. The approach: Launch a headless Chromium browser. Navigate to CSDN's login page. Perform one-time manual login via QR code. Serialize cookies to csdn_cookies.json . On subsequent runs, load the cookies and skip login. Go to the editor, inject Markdown content via DOM manipulation, fill the title, and click publish. Code snippet : import asyncio from playwright.async_api import async_playwright async def publish_to_csdn ( title : str , content_md : str ): async with async_playwright () as p : browser = await p . chromium . launch ( headless = True ) context = await browser . new_context ( storage_state = " csdn_cookies.json " if exists else None ) page = await context . new_page () await page . goto ( " https://mp.csdn.net/mp_blog/creation/editor " ) # Inject content await page . evaluate ( f ''' () => {{ const editor = document.querySelector( ' .editor-content ' ); if (editor) {{ editor.innerHTML = ` { escaped_content } `; editor.dispatchEvent(new Event( ' input ' , {{ bubbles: true }})); }} }} ''' ) # Fill title await page . fill ( ' #title-input ' , title ) await page . click ( ' button:has-text( " 发布 " ) ' ) await page . wait_for_url ( " **/mp_blog/manage/ar

2026-06-15 原文 →
AI 资讯

Automated Testing for SCORM E-Learning Packages Using Playwright — A Step-by-Step Guide

Most testing tutorials ignore e-learning completely. Here's how to build a Playwright test suite that validates your SCORM packages actually work across LMS platforms. Why E-Learning Testing Is Different If you've ever published a SCORM package to an LMS and watched it silently fail — no completion recorded, quiz scores vanishing, navigation broken — you know the pain. E-learning content doesn't behave like a typical web app. It runs inside an LMS-provided iframe, communicates through a JavaScript API (the SCORM Runtime), and its behavior changes depending on which LMS hosts it. Manual QA across even 3-4 LMS platforms is slow and error-prone. In this tutorial, I'll walk you through setting up Playwright to automate SCORM package testing — from basic content loading to verifying API calls and completion status. Prerequisites Before we start, make sure you have: Node.js 18+ installed Playwright ( npm init playwright@latest ) A SCORM 1.2 or 2004 package (a .zip file containing your e-learning content) A local LMS for testing — we'll use SCORM Cloud (free tier) or a simple SCORM API shim Step 1: Set Up a Local SCORM Runtime Shim Testing SCORM content requires an API that mimics what an LMS provides. Rather than spinning up a full Moodle instance, we'll create a lightweight shim. Create a file called scorm-api-shim.js : // scorm-api-shim.js // Mimics the SCORM 1.2 Runtime API that an LMS would expose window . API = { _data : {}, _initialized : false , _calls : [], LMSInitialize : function ( param ) { this . _initialized = true ; this . _calls . push ({ method : ' LMSInitialize ' , param , timestamp : Date . now () }); console . log ( ' [SCORM] LMSInitialize called ' ); return " true " ; }, LMSGetValue : function ( key ) { this . _calls . push ({ method : ' LMSGetValue ' , key , timestamp : Date . now () }); return this . _data [ key ] || "" ; }, LMSSetValue : function ( key , value ) { this . _data [ key ] = value ; this . _calls . push ({ method : ' LMSSetValue ' , key

2026-06-12 原文 →
AI 资讯

Playwright CLI for agent-driven workflows: sessions, debugging, and CI Sharding

Playwright has excellent tooling around browser automation, but most of the ecosystem still treats it as a test framework. For teams running AI coding agents and automated browser workflows, there is a different set of requirements: browser automation ↓ session persistence across runs ↓ debuggable traces when things go wrong ↓ parallel execution across CI shards The Playwright CLI directly addresses these gaps. It ships as a standalone npm package and exposes every browser operation as a CLI command; open, click, type, snapshot - without requiring a Node.js script or test runner. npm package: @playwright/cli GitHub: https://github.com/microsoft/playwright-cli The current implementation focuses on: session persistence with named instances and portable state video and trace recording built into every session CI sharding for parallel execution at scale session persistence The default behaviour keeps browser state in memory. Cookies and localStorage are preserved between CLI calls within the session, but cleared when the browser closes. For repeatable workflows, that breaks down fast — logging into an application before every run wastes time and introduces flakiness. Named sessions let you run multiple browser instances simultaneously and address them by name: playwright-cli -s=admin open https://app.example.com/admin playwright-cli -s=checkout open https://app.example.com/checkout Each session is an isolated browser instance. An agent can orchestrate workflows across multiple authenticated contexts without state leaking between them. The goal is straightforward: the same CLI binary should be able to maintain independent browser contexts for parallel workflows without requiring environment-specific configuration. The critical piece for CI and agent reuse is state persistence: log in once playwright-cli -s=admin open https://app.example.com/login playwright-cli -s=admin fill "#username" "admin" playwright-cli -s=admin fill "#password" "$ADMIN_PASS" playwright-cli -s=admi

2026-06-11 原文 →
AI 资讯

I automated my Gumroad product screenshots with Playwright

I automated my Gumroad product screenshots with Playwright I recently started packaging a few small frontend projects as digital products, and one surprisingly annoying part was preparing product screenshots. Manual screenshots quickly became messy: different browser sizes inconsistent cropping blurry images mobile screenshots were easy to get wrong Gumroad needed a square thumbnail every update meant taking screenshots again So I built a small local screenshot workflow with Next.js and Playwright. The workflow captures: desktop screenshots mobile screenshots square thumbnail images consistent PNG outputs route status checks basic console error reporting basic horizontal overflow checks The basic command flow is: npm run build npm run start npm run screenshots The script reads a simple config file, opens the configured local routes, captures each screenshot with consistent viewport settings, and exports the images into a predictable folder. For example: screenshots/gumroad/ landing.png dashboard.png template-preview.png mobile-preview.png thumbnail.png I found this especially useful when preparing Gumroad product galleries, because I could regenerate all product images after every UI change instead of taking screenshots manually. This is not a hosted screenshot service. It is just a local source-code workflow for people who want to generate product screenshots from their own Next.js pages. I packaged the workflow as a small Gumroad product here: https://remix410.gumroad.com/l/screenshot-automation-kit Curious how other developers handle product screenshots. Do you take them manually, use Playwright/Puppeteer, or use a design tool workflow?

2026-06-11 原文 →
AI 资讯

Locators & Web-First Assertions (Playwright + TypeScript, Ch.3)

In Chapter 2 we wrote our first tests and hit two bugs. Before we add more, we need the one skill everything else rests on: finding elements reliably . Get this right and your tests survive redesigns; get it wrong and they break every sprint. Code for this chapter is tagged ch-03 in the repo: https://github.com/aktibaba/playwright-qa-course — see src/tests/ui/locators.spec.ts . Locate the way a user perceives The brittle instinct is to grab elements by their structure — CSS classes, nth-child , XPath. All of that changes the moment a developer touches the markup. Playwright's recommended locators instead target what a user (and a screen reader) perceives: the role, the label, the visible text. Use them in this order of preference: getByRole — the role + accessible name (covers the vast majority of cases) getByLabel — form fields by their <label> getByPlaceholder — inputs without a label getByText — non-interactive content getByTestId — a deliberate data-testid , only when nothing semantic fits Here's the top of the priority list, live against Inkwell's home page: import { test , expect } from " @playwright/test " ; test ( " prefer role-based locators over CSS " , async ({ page }) => { await page . goto ( " / " ); await expect ( page . getByRole ( " button " , { name : " Global Feed " })). toBeVisible (); await expect ( page . getByRole ( " link " , { name : " Sign up " })). toBeVisible (); await expect ( page . getByRole ( " heading " , { name : " inkwell " })). toBeVisible (); }); getByRole("button", { name: "Global Feed" }) asserts two things at once — that an element with the button role exists and that its accessible name is "Global Feed". If a dev swaps the <div class="feed-btn"> for a real <button> , this locator keeps working; a CSS selector wouldn't. Strict mode is your friend Playwright locators are strict : if a locator matches more than one element, the action throws instead of silently picking the first. That catches ambiguous tests before they pick the

2026-06-08 原文 →
AI 资讯

Setup & Your First UI + API Tests (Playwright + TypeScript, Ch.2)

In Chapter 1 we argued that automation fails from a lack of structure , not a lack of tooling — and we met Inkwell , the dockerized app we test against. Now we install Playwright + TypeScript and write our first UI and API tests, deliberately simple. We'll also hit two real bugs along the way. I'm leaving them in on purpose — they're the exact problems the framework we build later is designed to prevent. Code for this chapter is tagged ch-02 in the repo: https://github.com/aktibaba/playwright-qa-course Before you start Make sure Inkwell is running (from Chapter 1): cd sut docker compose up -d --build --wait # web :3000, api :3001/api Install Playwright + TypeScript From the repo root: npm install -D @playwright/test typescript @types/node npx playwright install chromium That's it — Playwright bundles its own test runner, assertion library, and TypeScript support. No extra config to make .ts test files work. A minimal config — not a framework yet Two small files keep us honest from day one. First, never hard-code URLs in tests — put them in one place: // src/utils/env.ts export const env = { /** Inkwell SPA (nginx) — the UI base URL. */ webURL : process . env . WEB_URL ?? " http://localhost:3000 " , /** Inkwell API base, including the /api prefix. */ apiURL : process . env . API_URL ?? " http://localhost:3001/api " , } as const ; Then the Playwright config. We split tests into two projects — a fast api project and a Chromium ui project — because API tests need no browser and should run in milliseconds: // playwright.config.ts import { defineConfig , devices } from " @playwright/test " ; import { env } from " ./src/utils/env " ; export default defineConfig ({ testDir : " ./src/tests " , fullyParallel : true , reporter : " list " , use : { trace : " on-first-retry " , screenshot : " only-on-failure " }, projects : [ { name : " api " , testDir : " ./src/tests/api " , use : { baseURL : env . apiURL } }, { name : " ui " , testDir : " ./src/tests/ui " , use : { baseURL : e

2026-06-08 原文 →
AI 资讯

Why a Test Automation Framework? (Playwright + TypeScript, Ch.1)

Welcome to the first chapter of a hands-on course where we build a production-grade Playwright + TypeScript automation framework — covering both API and UI testing — against a real, dockerized web app you run on your own machine. This isn't a "here are 5 Playwright tips" post. By the end of the series you'll have a framework with the same shape a real QA team ships: layered, parallel-safe, authenticating once and reusing the session, seeding data through the API and verifying it in the UI, and running sharded in CI. We build it one chapter at a time, and every line of code is in a public repo you can clone and run. Who this is for You can read basic JavaScript (variables, functions, async/await ). That's it. No Playwright or TypeScript experience required — we introduce both from zero. You've maybe written a few UI tests before and felt them turn into a tangle. This course is about the structure that prevents that. How the course works Each chapter is one post in this series, in order. Read them top to bottom. There's a companion GitHub repo — the single source of truth for all code: 👉 https://github.com/aktibaba/playwright-qa-course The repo carries one git tag per chapter ( ch-01 , ch-02 , …) so you can check out the exact state of the code at any point and compare it to what you have. Every chapter ends with what changed, so you can either build along or just read the diff. Get the code and run the app We don't test toy pages. The course runs against Inkwell — a small but real React + Express + PostgreSQL blogging app (articles, comments, tags, follow/favorite, JWT auth). It lives in the same repo under sut/ ("system under test") and ships as a one-command Docker stack with deterministic reset / seed endpoints, so your tests never race startup or fight flaky data. You'll need Node.js 18+ and Docker . # 1. Clone the course repo git clone https://github.com/aktibaba/playwright-qa-course.git cd playwright-qa-course # 2. Start the app (db + API + web), wait until eve

2026-06-08 原文 →
AI 资讯

waitForResponse() timing: the one-line fix with a non-obvious mental model

The test hung for 30 seconds. The response had already fired. One moved line fixed it. The test hung for 30 seconds, then timed out. The browser had received the response. The page had loaded. The data was there. The test was still waiting. The wizard I was writing a helper to walk through a 4-step booking wizard. After clicking "Next" on step 1, the page does a full navigation — window.location.href to step 2. Step 2 immediately loads doctor data from the API. The helper looked like this: await Promise . all ([ page . waitForURL ( /step=2/ ), step1Next . click ()]); await page . waitForResponse ( r => r . url (). includes ( ' /doctors ' )); Standard pattern: wait for navigation, then wait for the data request. Timeout. Every time. What I checked first The URL pattern. Maybe /doctors wasn't matching. Opened the network tab. The request was there: GET /api/v1/doctors , 200, 47ms. Correct URL, correct response. The page looked fine. The data was rendered. The test said it was waiting for a response that had already happened. Added waitForLoadState . Still hung. Added an explicit waitForSelector for an element that was clearly on the page. That passed. Then waitForResponse hung again. The response existed. The test couldn't see it. What was actually happening page.waitForResponse() is not a query. It doesn't look at what happened. It registers a listener — from that exact moment forward — and waits for the next matching response. The sequence in my code: Promise.all resolves when the URL changes to step=2 By the time the URL changed, step 2 had already loaded Step 2 had already sent and received /api/v1/doctors Then waitForResponse registered its listener Now it's waiting for the next /doctors response Which never comes Playwright doesn't buffer missed events. If the response fired before the listener was registered — it's gone. The fix await Promise . all ([ page . waitForURL ( /step=2/ ), page . waitForResponse ( r => r . url (). includes ( ' /doctors ' )), step1Next

2026-06-05 原文 →
AI 资讯

I Built a One-Person AI QA Agency Using a Skill File and Local LLM

There is a specific failure mode in AI-assisted QA work that most tooling discussions skip entirely, and it shows up earliest when you are working solo on a real engagement. Every new chat session is stateless. You paste the ticket, describe the feature, explain your severity logic, set up the context, and by the time the AI is actually useful, you have rebuilt your methodology from scratch for the third time that week. That is not a workflow problem you fix with better prompts. It is an architecture problem, and the fix is a skill file. QAJourney has a full breakdown of this system at qajourney.net/ai-qa-workflow-for-real-projects, including the actual skill files as free downloads. The short version: a skill file is a context document you load as a system prompt. It carries your test surface tiers, your three-path testing framework, your bug report format, your severity and priority logic, your Playwright conventions, and an explicit definition of what the AI does and does not get to call. Load it once per session. The AI operates inside your methodology from the first message instead of a blank slate. The local LLM layer solves a different problem. On a freelance or retainer engagement, tickets contain real product logic and real client data. Sending that to a cloud API on every session is a data exposure question whether or not it rises to a compliance issue. Running Ollama locally with the same skill file as system context keeps the engagement data on the machine. For the output quality required on QA tasks, current 7B to 14B models are sufficient. The cost at zero marginal per token makes it infrastructure rather than a service you pay by the session. The three-role setup in the workflow: engineer as judgment layer, cloud AI loaded with the skill file for complex reasoning and active session output, local LLM for lightweight tasks and client data work. The skill file is the constant across all three. The part that took time to internalize: AI dev teams already

2026-06-01 原文 →