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

推荐订阅源

Cloudbric
Cloudbric
T
Threat Research - Cisco Blogs
Simon Willison's Weblog
Simon Willison's Weblog
AWS News Blog
AWS News Blog
P
Privacy & Cybersecurity Law Blog
H
Help Net Security
云风的 BLOG
云风的 BLOG
G
GRAHAM CLULEY
Spread Privacy
Spread Privacy
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
A
Arctic Wolf
Project Zero
Project Zero
Engineering at Meta
Engineering at Meta
P
Privacy International News Feed
Blog — PlanetScale
Blog — PlanetScale
Stack Overflow Blog
Stack Overflow Blog
M
MIT News - Artificial intelligence
The Register - Security
The Register - Security
Recorded Future
Recorded Future
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
C
Cisco Blogs
PCI Perspectives
PCI Perspectives
Recent Announcements
Recent Announcements
Martin Fowler
Martin Fowler
A
About on SuperTechFans
W
WeLiveSecurity
GbyAI
GbyAI
V
Vulnerabilities – Threatpost
The GitHub Blog
The GitHub Blog
D
Darknet – Hacking Tools, Hacker News & Cyber Security
C
Check Point Blog
Y
Y Combinator Blog
月光博客
月光博客
Scott Helme
Scott Helme
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
Google DeepMind News
Google DeepMind News
F
Fortinet All Blogs
U
Unit 42
G
Google Developers Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
Threatpost
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Google Online Security Blog
Google Online Security Blog
Recent Commits to openclaw:main
Recent Commits to openclaw:main
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Cisco Talos Blog
Cisco Talos Blog
博客园 - 三生石上(FineUI控件)
Hugging Face - Blog
Hugging Face - Blog
MongoDB | Blog
MongoDB | Blog
博客园 - 司徒正美

Echo JS

Desktop apps Introducing mermaid-lint: Stop Shipping Broken Diagrams How We Cut Slow Responses by 80% Migrating to Next.js App Router The quiet problem with unnecessary async - Matt Smith GitHub - ant-design/ant-design-cli: Ant Design on your command line. Query component knowledge, analyze project usage, and guide migrations — fully offline. React Performance Isn’t About useMemo — It’s About Render Boundaries T2 No Escape Hatches · Prickles GitHub - auto-agent-protocol/auto-agent-protocol: Automotive Agent Protocol GitHub - coactionjs/coaction: Zustand-style state management with built-in render tracking and cached computed state GitHub - williamtroup/Rattribute.js: ❓ A lightweight JavaScript library for automatically changing HTML element attributes based on responsive screen sizes. GitHub - williamtroup/Rink.js: 🔗 A JavaScript library for generating responsive HTML link targets. SVAR Kanban: Interactive Task Board Component for Svelte, React, and Vue Performance Cost of Popular 3rd Party Scripts date-light - Tiny date utilities for JavaScript When React Hooks Stop Scaling: Moving Complex State to Zustand - Oren Farhi GitHub - jskits/loggerjs: A faster, more powerful isomorphic logger Out Loud — Free AI Text to Speech Pocket DB — Embedded single-file NoSQL for Node.js billboard.js 4.0 release: Canvas rendering mode, 94.3% faster! GitHub - thegruber/linkpeek: Secure TypeScript link preview and URL metadata extractor for Open Graph, Twitter Cards, JSON-LD, Node/Bun/Deno/edge. GitHub - evoluteur/healing-frequencies: Play the healing frequencies of various sets of tuning forks: Solfeggio, Organs, Mineral nutrients, Ohm, Chakras, Cosmic octave, Otto, DNA nucleotides... or custom. Animated sine waves - 27 lines of pure JavaScript Framework | Neutralinojs GitHub - AllThingsSmitty/typescript-tips-everyone-should-know: ✅ A curated collection of practical TypeScript patterns that improve safety, readability, maintainability, and developer experience. 🧠 Heat.js : JavaScript Heat Map GitHub - iDev-Games/State-JS: State.js is a CSS‑reactive framework that makes UI state and updates flow through CSS instead of JavaScript logic. GitHub - yankouskia/gameplate: :video_game: Boilerplate for creating game with WebGL & Redux :game_die: GitHub - yankouskia/get-browser: 💻 Lightweight tool to identify the browser (mobile+desktop detection)📱 GitHub - yankouskia/is-incognito-mode: Identify whether browser is in incognito mode 👀
Uncovering the Magic Behind Playwright
2026-05-23 · via Echo JS

One of the best ways to really learn a tool is to understand how it works internally. With most JavaScript libraries, I can usually get a rough idea of the implementation from the API design alone, without opening the source code. Playwright’s fixtures API, however, was harder to reason about. A minimal test looks like this:

import { test, expect } from "@playwright/test";

test("basic test", async ({ page }) => {

await page.goto("https://playwright.dev/");

await expect(page).toHaveTitle(/Playwright/);

});

In this example, we request the page fixture from Playwright and use it in the test. At first glance, nothing unusual is happening. Playwright passes an object with a set of fixtures, including page. A simplified implementation of the test function might look like this:

async function test(title, body) {

const browser = await firefox.launch();

const context = await browser.newContext();

const page = await context.newPage();

const fixtures = {

page,

context,

browser

};

body(fixtures);

// ...teardown code here...

}

If only it were that simple. The documentation says:

Fixtures are on-demand — you can define as many fixtures as you’d like, and Playwright Test will setup only the ones needed by your test and nothing else.

In other words, Playwright fixtures are lazy. If page is not used, Playwright skips its initialization and saves some test execution time. But in the example above, every field of the fixtures object is initialized before the test starts. How can we avoid that? How does Playwright make fixtures lazy?

Proxy-based solution

One possible solution is to use Proxy to track which fields are accessed inside the test body. That would let us initialize only the fields that are actually needed and skip the rest. For simplicity, let’s use getters in the example below. A Proxy-based version would be a more general form of the same idea:

async function test(title, body) {

const browser = await firefox.launch();

const context = await browser.newContext();

const fixtures = {

get page() {

return context.newPage();

},

context,

browser

};

body(fixtures);

// ...teardown code here...

}

Now page is initialized only if the test explicitly accesses that field. It looks as if the problem is solved and fixtures are lazy. But this no longer behaves like Playwright’s API. The context.newPage() method is asynchronous and returns a Promise, so the user would have to await the fixture before using it. That makes the API less convenient:

import { test, expect } from "@playwright/test";

test("basic test", async (fixtures) => {

const page = await fixtures.page;

await page.goto("https://playwright.dev/");

await expect(page).toHaveTitle(/Playwright/);

});

In Playwright, however, the page fixture is already initialized before the test body runs, so no await is needed. How can we get the same behavior? We need to know whether the test uses page before we prepare it. But if we rely on property access to detect that, we have to run the test first. This gives us the classic chicken-and-egg problem. So how does Playwright solve it?

If we zoom out a little, the problem becomes a simple question: how can we know which parameters a function accepts without calling it?

How to get function parameters without calling the function

The missing piece is that Playwright can figure out which fixtures fields the test function needs without calling that function. The documentation says:

Playwright Test looks at each test declaration, analyses the set of fixtures the test needs and prepares those fixtures specifically for the test.

There is another important detail: Playwright effectively forces us to access fixtures through parameter destructuring:

// ✅ Valid use of fixtures

test("correct", async ({ page }) => {});

// ❌ This will throw an error

test("not correct", async (fixtures) => {

const { page } = fixtures;

});

If we don’t follow this pattern, Playwright shows the error “First argument must use the object destructuring pattern”. This requirement is almost certainly tied to how Playwright understands which fixtures we are requesting. At first, I assumed Playwright did this in a preliminary analysis step: parse the file, find test functions in the AST, and extract their arguments. In practice, there is no separate preparation phase. Playwright reads the declaration while the tests are being loaded and registered.

The key to reading function parameters without calling the function is Function.prototype.toString(). It gives Playwright the function’s source code as a string. From there, Playwright can parse something like async ({ page }) => {...} and extract the fixture names used by the test.

Here is a simplified version of innerFixtureParameterNames:

function splitByComma(str) {

20 collapsed lines

const result = [];

const stack = [];

let start = 0;

for (let i = 0; i < str.length; i++) {

if (str[i] === "{" || str[i] === "[") {

stack.push(str[i] === "{" ? "}" : "]");

} else if (str[i] === stack[stack.length - 1]) {

stack.pop();

} else if (!stack.length && str[i] === ",") {

const token = str.substring(start, i).trim();

if (token) result.push(token);

start = i + 1;

}

}

const lastToken = str.substring(start).trim();

if (lastToken) result.push(lastToken);

return result;

}

function parseParams(params) {

23 collapsed lines

if (!params) return [];

const [firstParam] = splitByComma(params);

if (firstParam[0] !== "{" || firstParam[firstParam.length - 1] !== "}") {

throw new Error(`First argument must use the object destructuring pattern`);

}

const props = splitByComma(

firstParam.substring(1, firstParam.length - 1)

).map((prop) => {

const colon = prop.indexOf(":");

return colon === -1 ? prop.trim() : prop.substring(0, colon).trim();

});

const restProperty = props.find((prop) => prop.startsWith("..."));

if (restProperty) {

throw new Error(`Rest properties are not supported in fixture parameters`);

}

return props;

}

function innerFixtureParameterNames(fn) {

const text = fn.toString();

const match = text.match(/(?:async)?(?:\s+function)?[^(]*\(([^)]*)/);

if (!match) return [];

const trimmedParams = match[1].trim();

return parseParams(trimmedParams);

}

This explains the “First argument must use the object destructuring pattern” requirement. Without it, those parameters would be much harder to extract. The approach is clever, but it also made me wonder how transparent it is for the user (the API becomes more magical), and how reliable it would be in edge cases.

Testing the boundaries of the fixtures API

Those reliability doubts came from a few potential weak spots.

Different runtimes

Function.prototype.toString() may look suspicious, but it is part of the standard and is well supported by browsers and server-side runtimes. This makes it a reasonable dependency for Playwright, even though the whole idea still feels unusual.

Different kinds of functions in JavaScript

JavaScript gives us many ways to declare functions:

function fn({ page, browser }) {}

async function asyncFn({ page, browser }) {}

function* generatorFn({ page, browser }) {}

const arrowFn = ({ page, browser }) => {};

const asyncArrowFn = async ({ page, browser }) => {};

Because of a carefully chosen regular expression, innerFixtureParameterNames supports all of these variants:

const match = text.match(/(?:async)?(?:\s+function)?[^(]*\(([^)]*)/);

Minifiers

In practice, source code often reaches the runtime only after a build step. Transformers and minifiers may rewrite function signatures, especially in browser-oriented code. Can that break this API? To check, I tried Terser and esbuild with the minify flag. The output looked like this:

// before

export function fn({ foo, bar }) {}

export async function asyncFn({ foo, bar }) {}

export function* generatorFn({ foo, bar }) {}

export const arrowFn = ({ foo, bar }) => {};

export const asyncArrowFn = async ({ foo, bar }) => {};

// after

export function fn({ foo: o, bar: n }) {}

export async function asyncFn({ foo: o, bar: n }) {}

export function* generatorFn({ foo: o, bar: n }) {}

export const arrowFn = ({ foo: o, bar: n }) => {};

export const asyncArrowFn = async ({ foo: o, bar: n }) => {};

In this experiment, the minifiers only changed function signatures by replacing longer identifiers like foo and bar with shorter names like o and n. innerFixtureParameterNames accounts for this syntax and handles the code correctly. Still, I am not fully convinced that every possible transformation is safe for this API.

Conclusion

Extracting function parameters before running the function is a clever and interesting approach. It improves the DX and makes the test API feel more direct. At the same time, it feels a little magical, which can violate the principle of least astonishment.

It also makes some patterns harder to use. Function composition is one example:

function noThrow(fn) {

return () => {

try {

return fn();

} catch {}

};

}

function fn({ foo, bar }) {}

innerFixtureParameterNames(noThrow(fn));

In this case, innerFixtureParameterNames predictably fails with an error, because it receives a wrapper function instead of fn itself. For Playwright, though, this scenario is not especially relevant.

I think the Playwright team made a good choice by adopting this API: it fits the test function very well. But I can’t easily think of another library where the same approach would feel just as justified. Even after writing this article, I still have mixed feelings about it. There is a little more magic here than I would like.