@a11y-pulse/tab-orchestrator 0.3.0 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE.md CHANGED
@@ -1,166 +1,21 @@
1
- Copyright (c) 2026-present, A11y Pulse Limited (https://www.a11ypulse.com/). All rights reserved.
2
-
3
- # PolyForm Shield License 1.0.0
4
-
5
- <https://polyformproject.org/licenses/shield/1.0.0>
6
-
7
- ## Acceptance
8
-
9
- In order to get any license under these terms, you must agree
10
- to them as both strict obligations and conditions to all
11
- your licenses.
12
-
13
- ## Copyright License
14
-
15
- The licensor grants you a copyright license for the
16
- software to do everything you might do with the software
17
- that would otherwise infringe the licensor's copyright
18
- in it for any permitted purpose. However, you may
19
- only distribute the software according to [Distribution
20
- License](#distribution-license) and make changes or new works
21
- based on the software according to [Changes and New Works
22
- License](#changes-and-new-works-license).
23
-
24
- ## Distribution License
25
-
26
- The licensor grants you an additional copyright license
27
- to distribute copies of the software. Your license
28
- to distribute covers distributing the software with
29
- changes and new works permitted by [Changes and New Works
30
- License](#changes-and-new-works-license).
31
-
32
- ## Notices
33
-
34
- You must ensure that anyone who gets a copy of any part of
35
- the software from you also gets a copy of these terms or the
36
- URL for them above, as well as copies of any plain-text lines
37
- beginning with `Required Notice:` that the licensor provided
38
- with the software. For example:
39
-
40
- > Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
41
-
42
- ## Changes and New Works License
43
-
44
- The licensor grants you an additional copyright license to
45
- make changes and new works based on the software for any
46
- permitted purpose.
47
-
48
- ## Patent License
49
-
50
- The licensor grants you a patent license for the software that
51
- covers patent claims the licensor can license, or becomes able
52
- to license, that you would infringe by using the software.
53
-
54
- ## Noncompete
55
-
56
- Any purpose is a permitted purpose, except for providing any
57
- product that competes with the software or any product the
58
- licensor or any of its affiliates provides using the software.
59
-
60
- ## Competition
61
-
62
- Goods and services compete even when they provide functionality
63
- through different kinds of interfaces or for different technical
64
- platforms. Applications can compete with services, libraries
65
- with plugins, frameworks with development tools, and so on,
66
- even if they're written in different programming languages
67
- or for different computer architectures. Goods and services
68
- compete even when provided free of charge. If you market a
69
- product as a practical substitute for the software or another
70
- product, it definitely competes.
71
-
72
- ## New Products
73
-
74
- If you are using the software to provide a product that does
75
- not compete, but the licensor or any of its affiliates brings
76
- your product into competition by providing a new version of
77
- the software or another product using the software, you may
78
- continue using versions of the software available under these
79
- terms beforehand to provide your competing product, but not
80
- any later versions.
81
-
82
- ## Discontinued Products
83
-
84
- You may begin using the software to compete with a product
85
- or service that the licensor or any of its affiliates has
86
- stopped providing, unless the licensor includes a plain-text
87
- line beginning with `Licensor Line of Business:` with the
88
- software that mentions that line of business. For example:
89
-
90
- > Licensor Line of Business: YoyodyneCMS Content Management
91
- System (http://example.com/cms)
92
-
93
- ## Sales of Business
94
-
95
- If the licensor or any of its affiliates sells a line of
96
- business developing the software or using the software
97
- to provide a product, the buyer can also enforce
98
- [Noncompete](#noncompete) for that product.
99
-
100
- ## Fair Use
101
-
102
- You may have "fair use" rights for the software under the
103
- law. These terms do not limit them.
104
-
105
- ## No Other Rights
106
-
107
- These terms do not allow you to sublicense or transfer any of
108
- your licenses to anyone else, or prevent the licensor from
109
- granting licenses to anyone else. These terms do not imply
110
- any other licenses.
111
-
112
- ## Patent Defense
113
-
114
- If you make any written claim that the software infringes or
115
- contributes to infringement of any patent, your patent license
116
- for the software granted under these terms ends immediately. If
117
- your company makes such a claim, your patent license ends
118
- immediately for work on behalf of your company.
119
-
120
- ## Violations
121
-
122
- The first time you are notified in writing that you have
123
- violated any of these terms, or done anything with the software
124
- not covered by your licenses, your licenses can nonetheless
125
- continue if you come into full compliance with these terms,
126
- and take practical steps to correct past violations, within
127
- 32 days of receiving notice. Otherwise, all your licenses
128
- end immediately.
129
-
130
- ## No Liability
131
-
132
- ***As far as the law allows, the software comes as is, without
133
- any warranty or condition, and the licensor will not be liable
134
- to you for any damages arising out of these terms or the use
135
- or nature of the software, under any kind of legal claim.***
136
-
137
- ## Definitions
138
-
139
- The **licensor** is the individual or entity offering these
140
- terms, and the **software** is the software the licensor makes
141
- available under these terms.
142
-
143
- A **product** can be a good or service, or a combination
144
- of them.
145
-
146
- **You** refers to the individual or entity agreeing to these
147
- terms.
148
-
149
- **Your company** is any legal entity, sole proprietorship,
150
- or other kind of organization that you work for, plus all
151
- its affiliates.
152
-
153
- **Affiliates** means the other organizations than an
154
- organization has control over, is under the control of, or is
155
- under common control with.
156
-
157
- **Control** means ownership of substantially all the assets of
158
- an entity, or the power to direct its management and policies
159
- by vote, contract, or otherwise. Control can be direct or
160
- indirect.
161
-
162
- **Your licenses** are all the licenses granted to you for the
163
- software under these terms.
164
-
165
- **Use** means anything you do with the software requiring one
166
- of your licenses.
1
+ # MIT License
2
+
3
+ Copyright (c) 2026-present, A11y Pulse (https://www.a11ypulse.com/)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -2,13 +2,13 @@
2
2
 
3
3
  [![npm version](https://img.shields.io/npm/v/@a11y-pulse/tab-orchestrator)](https://www.npmjs.com/package/@a11y-pulse/tab-orchestrator)
4
4
  [![CI](https://github.com/A11y-Pulse/audits/actions/workflows/ci.yml/badge.svg)](https://github.com/A11y-Pulse/audits/actions/workflows/ci.yml)
5
- [![License: PolyForm Shield 1.0.0](https://img.shields.io/badge/license-PolyForm%20Shield%201.0.0-blue)](./LICENSE.md)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green)](./LICENSE.md)
6
6
 
7
- A shared Tab-session runner for A11y Pulse's keyboard-driven accessibility audits. It drives a page through its focusable elements once — pressing Tab, tracking visited elements, capturing screenshots and styles on demand — while one or more **consumers** each score the tab stops for their own WCAG success criterion. This lets audits like [`@a11y-pulse/focus-appearance-audit`](../focus-appearance-audit) (WCAG 2.4.7) share a single tab loop with sibling audits instead of each re-tabbing the page from scratch.
7
+ A shared Tab-session runner for A11y Pulse's keyboard-driven accessibility audits. It drives a page through its focusable elements once (pressing Tab, tracking visited elements, capturing screenshots and styles on demand) while one or more **consumers** each score the tab stops for their own WCAG success criterion. This lets audits like [`@a11y-pulse/focus-appearance-audit`](../focus-appearance-audit) (WCAG 2.4.7) share a single tab loop with sibling audits instead of each re-tabbing the page from scratch.
8
8
 
9
9
  It is not an audit itself: it has no opinion on what "passing" means. It only owns the mechanics of moving focus, tracking cycle completion, and capturing the primitives (computed styles, clipped screenshots, unfocused/focused pairs) that consumers need.
10
10
 
11
- This package was developed by [A11y Pulse](https://www.a11ypulse.com/) for its accessibility monitoring service. It is released as source-available under the [PolyForm Shield License 1.0.0](#license).
11
+ This package was developed by [A11y Pulse](https://www.a11ypulse.com/) for its accessibility monitoring service. It is released under the [MIT License](#license).
12
12
 
13
13
  ## Install
14
14
 
@@ -35,9 +35,9 @@ await orchestrator.run();
35
35
  - `.attach(consumer)` registers a `TabConsumer`. Attaching after `run()` has started throws.
36
36
  - `.run()` presses Tab repeatedly, dispatching each `TabStopSnapshot` to every still-attached consumer, until either a full tab cycle completes, focus is lost, navigation happens, or every consumer has disconnected. Calling it twice throws; calling it with zero attached consumers returns immediately without tabbing.
37
37
 
38
- Each consumer declares the `capabilities` it needs (e.g. `baselineStyles`, `unfocusedPair`) and can disconnect independently once it has what it needs (say, after hitting its own element limit) — the orchestrator keeps driving the remaining consumers. Per-stop work like unfocused-pair screenshots is only captured if a still-attached consumer actually asks for it that stop.
38
+ Each consumer declares the `capabilities` it needs (e.g. `baselineStyles`, `unfocusedPair`) and can disconnect independently once it has what it needs (say, after hitting its own element limit). The orchestrator keeps driving the remaining consumers. Per-stop work like unfocused-pair screenshots is only captured if a still-attached consumer actually asks for it that stop.
39
39
 
40
- Most callers won't build a `TabConsumer` by hand — audit packages export a `createXAudit(options)` helper that returns one, plus a `runXAudit(adaptor, options)` convenience wrapper that builds a private single-consumer orchestrator for you. See [`@a11y-pulse/focus-appearance-audit`'s "Shared tab session"](../focus-appearance-audit#shared-tab-session) section for a worked example.
40
+ Most callers won't build a `TabConsumer` by hand. Audit packages export a `createXAudit(options)` helper that returns one, plus a `runXAudit(adaptor, options)` convenience wrapper that builds a private single-consumer orchestrator for you. See [`@a11y-pulse/focus-appearance-audit`'s "Shared tab session"](../focus-appearance-audit#shared-tab-session) section for a worked example.
41
41
 
42
42
  ## `BrowserAdaptor`
43
43
 
@@ -50,7 +50,7 @@ The orchestrator drives a page through an **adaptor**: a small interface of prim
50
50
  | `disposeRef(ref)` | Releases a handle previously returned by `evaluateHandle`. |
51
51
  | `pressTab()` | Presses the Tab key, advancing focus to the next focusable element. |
52
52
  | `screenshotClip(clip)` | Screenshots a clipped region of the page (`{ x, y, width, height }`) and returns PNG bytes. |
53
- | `ensureFocusReporting()` | Ensures the page reports focus for its lifetime — in particular that `document.hasFocus()` works and `:focus` styles apply, even when the page is not the foreground tab/window. Must not throw. |
53
+ | `ensureFocusReporting()` | Ensures the page reports focus for its lifetime, in particular that `document.hasFocus()` works and `:focus` styles apply, even when the page is not the foreground tab/window. Must not throw. |
54
54
 
55
55
  The orchestrator is the only thing that mutates the page through this adaptor during a session; consumers score snapshots, they don't call `pressTab`, take screenshots, or install observers themselves.
56
56
 
@@ -84,10 +84,7 @@ Trusted Publisher on npm must stay configured for:
84
84
 
85
85
  ## License
86
86
 
87
- Released under the [PolyForm Shield License 1.0.0](./LICENSE.md), in plain language:
87
+ [MIT](./LICENSE.md). Use it however you like, including in commercial and competing products.
88
88
 
89
- - **Source-available.** The source is public and you can read, fork, and modify it.
90
- - **Permitted for non-competing use.** You can use this package freely in your own products and services, as long as they don't compete with A11y Pulse.
91
- - **Competing products are forbidden.** You may not use this software (or a modified version of it) to build a product or service that competes with A11y Pulse's accessibility monitoring offering.
92
-
93
- See [LICENSE.md](./LICENSE.md) for the full, binding terms.
89
+ The audit packages in this repository are licensed separately, under the
90
+ [PolyForm Shield License 1.0.0](../../LICENSE.md).
package/dist/adaptor.d.ts CHANGED
@@ -9,8 +9,8 @@ export type Rect = {
9
9
  */
10
10
  export type ElementRef = unknown;
11
11
  /**
12
- * A framework-specific adaptor that provides the primitives needed by the audit. An adaptor wraps
13
- * a single page in a web browser.
12
+ * A framework-specific adaptor that provides the primitives needed by the audit. An adaptor wraps a
13
+ * single page in a web browser.
14
14
  */
15
15
  export interface BrowserAdaptor {
16
16
  /**
@@ -30,10 +30,9 @@ export interface BrowserAdaptor {
30
30
  */
31
31
  pressTab(): Promise<void>;
32
32
  /**
33
- * Screenshot a clipped region of the page. `scale` is the device scale
34
- * factor (bitmap pixels per CSS pixel), multiplied against whatever
35
- * deviceScaleFactor the page itself currently has (Puppeteer does not
36
- * replace it). The caller is responsible for the page being at a known
33
+ * Screenshot a clipped region of the page. `scale` is the device scale factor (bitmap pixels per
34
+ * CSS pixel), multiplied against whatever deviceScaleFactor the page itself currently has
35
+ * (Puppeteer does not replace it). The caller is responsible for the page being at a known
37
36
  * deviceScaleFactor before invoking this.
38
37
  */
39
38
  screenshotClip(clip: Rect, scale?: number): Promise<Uint8Array>;
@@ -41,8 +40,7 @@ export interface BrowserAdaptor {
41
40
  readonly screenshotClipScale?: number;
42
41
  /**
43
42
  * Ensure the page reports focus for its lifetime. In particular, `document.hasFocus()` must
44
- * function correctly and :focus styles must be applied, even when the page is not the
45
- * foreground.
43
+ * function correctly and :focus styles must be applied, even when the page is not the foreground.
46
44
  *
47
45
  * This function must not throw.
48
46
  */
@@ -1,5 +1,5 @@
1
1
  // src/adaptors/puppeteer.ts
2
- var focusEmulationEnabled = /* @__PURE__ */ new WeakSet();
2
+ var focusEmulationSessions = /* @__PURE__ */ new WeakMap();
3
3
  var PuppeteerAdaptor = class {
4
4
  constructor(page) {
5
5
  this.page = page;
@@ -29,20 +29,24 @@ var PuppeteerAdaptor = class {
29
29
  });
30
30
  }
31
31
  async ensureFocusReporting() {
32
- if (focusEmulationEnabled.has(this.page)) {
33
- return;
34
- }
35
32
  try {
33
+ const existing = focusEmulationSessions.get(this.page);
34
+ if (existing) {
35
+ await existing.send("Emulation.setFocusEmulationEnabled", {
36
+ enabled: false
37
+ });
38
+ await existing.send("Emulation.setFocusEmulationEnabled", {
39
+ enabled: true
40
+ });
41
+ return;
42
+ }
36
43
  const client = await this.page.createCDPSession();
37
44
  await client.send("Emulation.setFocusEmulationEnabled", {
38
45
  enabled: true
39
46
  });
40
- focusEmulationEnabled.add(this.page);
47
+ focusEmulationSessions.set(this.page, client);
41
48
  } catch (error) {
42
- console.warn(
43
- "Could not enable focus emulation for the tab orchestrator",
44
- { error }
45
- );
49
+ console.warn("Could not enable focus emulation for the tab orchestrator", { error });
46
50
  }
47
51
  }
48
52
  };
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/adaptors/puppeteer.ts"],"sourcesContent":["import type { JSHandle, Page } from \"puppeteer\";\nimport type { BrowserAdaptor, ElementRef, Rect } from \"../adaptor\";\n\n// Pages that already have focus emulation enabled. A fresh adaptor is created per\n// audit run, but a page is often reused across runs (e.g. one page per snapshot,\n// many checks), so without this each run would open another CDP session that is\n// never detached. The emulation persists for the page's lifetime, so enabling it\n// once is enough. (Detaching the session instead is not an option — Chrome clears\n// the emulation when the CDP client disconnects.)\nconst focusEmulationEnabled = new WeakSet<Page>();\n\n/** A BrowserAdaptor backed by a Puppeteer Page. */\nexport class PuppeteerAdaptor implements BrowserAdaptor {\n\treadonly screenshotClipScale = 2;\n\n\tconstructor(private readonly page: Page) {}\n\n\tevaluate<T>(\n\t\t// biome-ignore lint/suspicious/noExplicitAny: the page-evaluated fn accepts arbitrary serialised args and element handles.\n\t\tfn: (...args: any[]) => T | Promise<T>,\n\t\t...args: unknown[]\n\t): Promise<T> {\n\t\t// Puppeteer resolves any JSHandle passed as an arg to the live element.\n\t\treturn this.page.evaluate(fn as never, ...(args as never[])) as Promise<T>;\n\t}\n\n\tevaluateHandle(fn: () => Element | null): Promise<ElementRef> {\n\t\treturn this.page.evaluateHandle(fn);\n\t}\n\n\tasync disposeRef(ref: ElementRef): Promise<void> {\n\t\tawait (ref as JSHandle).dispose();\n\t}\n\n\tasync pressTab(): Promise<void> {\n\t\tawait this.page.keyboard.press(\"Tab\");\n\t}\n\n\tasync screenshotClip(clip: Rect, scale = 1): Promise<Uint8Array> {\n\t\treturn (await this.page.screenshot({\n\t\t\ttype: \"png\",\n\t\t\toptimizeForSpeed: true,\n\t\t\tclip: {\n\t\t\t\t...clip,\n\t\t\t\tscale,\n\t\t\t},\n\t\t})) as Uint8Array;\n\t}\n\n\tasync ensureFocusReporting(): Promise<void> {\n\t\t// Pages only report focus while they are the foreground tab. Since this browser instance\n\t\t// is likely running in the background, we need to enable focus emulation to get accurate\n\t\t// results.\n\t\t//\n\t\t// Note this function does not throw if enabling emulation fails; it is a best-effort.\n\t\tif (focusEmulationEnabled.has(this.page)) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tconst client = await this.page.createCDPSession();\n\t\t\tawait client.send(\"Emulation.setFocusEmulationEnabled\", {\n\t\t\t\tenabled: true,\n\t\t\t});\n\t\t\tfocusEmulationEnabled.add(this.page);\n\t\t} catch (error) {\n\t\t\tconsole.warn(\n\t\t\t\t\"Could not enable focus emulation for the tab orchestrator\",\n\t\t\t\t{ error },\n\t\t\t);\n\t\t}\n\t}\n}\n"],"mappings":";AASA,IAAM,wBAAwB,oBAAI,QAAc;AAGzC,IAAM,mBAAN,MAAiD;AAAA,EAGvD,YAA6B,MAAY;AAAZ;AAAA,EAAa;AAAA,EAAb;AAAA,EAFpB,sBAAsB;AAAA,EAI/B,SAEC,OACG,MACU;AAEb,WAAO,KAAK,KAAK,SAAS,IAAa,GAAI,IAAgB;AAAA,EAC5D;AAAA,EAEA,eAAe,IAA+C;AAC7D,WAAO,KAAK,KAAK,eAAe,EAAE;AAAA,EACnC;AAAA,EAEA,MAAM,WAAW,KAAgC;AAChD,UAAO,IAAiB,QAAQ;AAAA,EACjC;AAAA,EAEA,MAAM,WAA0B;AAC/B,UAAM,KAAK,KAAK,SAAS,MAAM,KAAK;AAAA,EACrC;AAAA,EAEA,MAAM,eAAe,MAAY,QAAQ,GAAwB;AAChE,WAAQ,MAAM,KAAK,KAAK,WAAW;AAAA,MAClC,MAAM;AAAA,MACN,kBAAkB;AAAA,MAClB,MAAM;AAAA,QACL,GAAG;AAAA,QACH;AAAA,MACD;AAAA,IACD,CAAC;AAAA,EACF;AAAA,EAEA,MAAM,uBAAsC;AAM3C,QAAI,sBAAsB,IAAI,KAAK,IAAI,GAAG;AACzC;AAAA,IACD;AAEA,QAAI;AACH,YAAM,SAAS,MAAM,KAAK,KAAK,iBAAiB;AAChD,YAAM,OAAO,KAAK,sCAAsC;AAAA,QACvD,SAAS;AAAA,MACV,CAAC;AACD,4BAAsB,IAAI,KAAK,IAAI;AAAA,IACpC,SAAS,OAAO;AACf,cAAQ;AAAA,QACP;AAAA,QACA,EAAE,MAAM;AAAA,MACT;AAAA,IACD;AAAA,EACD;AACD;","names":[]}
1
+ {"version":3,"sources":["../../src/adaptors/puppeteer.ts"],"sourcesContent":["import type { CDPSession, JSHandle, Page } from \"puppeteer\";\nimport type { BrowserAdaptor, ElementRef, Rect } from \"../adaptor\";\n\n// The CDP session holding focus emulation for each page. A fresh adaptor is created per audit run,\n// but a page often outlives one run, so without this each run would open another CDP session that\n// is never detached. The session is kept attached for the page's lifetime: Chrome clears the\n// emulation when the CDP client disconnects.\nconst focusEmulationSessions = new WeakMap<Page, CDPSession>();\n\n/** A BrowserAdaptor backed by a Puppeteer Page. */\nexport class PuppeteerAdaptor implements BrowserAdaptor {\n\treadonly screenshotClipScale = 2;\n\n\tconstructor(private readonly page: Page) {}\n\n\tevaluate<T>(\n\t\t// biome-ignore lint/suspicious/noExplicitAny: the page-evaluated fn accepts arbitrary serialised args and element handles.\n\t\tfn: (...args: any[]) => T | Promise<T>,\n\t\t...args: unknown[]\n\t): Promise<T> {\n\t\t// Puppeteer resolves any JSHandle passed as an arg to the live element.\n\t\treturn this.page.evaluate(fn as never, ...(args as never[])) as Promise<T>;\n\t}\n\n\tevaluateHandle(fn: () => Element | null): Promise<ElementRef> {\n\t\treturn this.page.evaluateHandle(fn);\n\t}\n\n\tasync disposeRef(ref: ElementRef): Promise<void> {\n\t\tawait (ref as JSHandle).dispose();\n\t}\n\n\tasync pressTab(): Promise<void> {\n\t\tawait this.page.keyboard.press(\"Tab\");\n\t}\n\n\tasync screenshotClip(clip: Rect, scale = 1): Promise<Uint8Array> {\n\t\treturn (await this.page.screenshot({\n\t\t\ttype: \"png\",\n\t\t\toptimizeForSpeed: true,\n\t\t\tclip: {\n\t\t\t\t...clip,\n\t\t\t\tscale,\n\t\t\t},\n\t\t})) as Uint8Array;\n\t}\n\n\tasync ensureFocusReporting(): Promise<void> {\n\t\t// Pages only report focus while they are the foreground tab. Since this browser instance is\n\t\t// likely running in the background, we need to enable focus emulation to get accurate results.\n\t\t//\n\t\t// Note this function does not throw if enabling emulation fails; it is a best-effort.\n\t\ttry {\n\t\t\tconst existing = focusEmulationSessions.get(this.page);\n\n\t\t\tif (existing) {\n\t\t\t\t// Chromium ignores a redundant enable, so re-asserting has to toggle.\n\t\t\t\tawait existing.send(\"Emulation.setFocusEmulationEnabled\", {\n\t\t\t\t\tenabled: false,\n\t\t\t\t});\n\t\t\t\tawait existing.send(\"Emulation.setFocusEmulationEnabled\", {\n\t\t\t\t\tenabled: true,\n\t\t\t\t});\n\n\t\t\t\treturn;\n\t\t\t}\n\n\t\t\tconst client = await this.page.createCDPSession();\n\t\t\tawait client.send(\"Emulation.setFocusEmulationEnabled\", {\n\t\t\t\tenabled: true,\n\t\t\t});\n\t\t\tfocusEmulationSessions.set(this.page, client);\n\t\t} catch (error) {\n\t\t\tconsole.warn(\"Could not enable focus emulation for the tab orchestrator\", { error });\n\t\t}\n\t}\n}\n"],"mappings":";AAOA,IAAM,yBAAyB,oBAAI,QAA0B;AAGtD,IAAM,mBAAN,MAAiD;AAAA,EAGvD,YAA6B,MAAY;AAAZ;AAAA,EAAa;AAAA,EAAb;AAAA,EAFpB,sBAAsB;AAAA,EAI/B,SAEC,OACG,MACU;AAEb,WAAO,KAAK,KAAK,SAAS,IAAa,GAAI,IAAgB;AAAA,EAC5D;AAAA,EAEA,eAAe,IAA+C;AAC7D,WAAO,KAAK,KAAK,eAAe,EAAE;AAAA,EACnC;AAAA,EAEA,MAAM,WAAW,KAAgC;AAChD,UAAO,IAAiB,QAAQ;AAAA,EACjC;AAAA,EAEA,MAAM,WAA0B;AAC/B,UAAM,KAAK,KAAK,SAAS,MAAM,KAAK;AAAA,EACrC;AAAA,EAEA,MAAM,eAAe,MAAY,QAAQ,GAAwB;AAChE,WAAQ,MAAM,KAAK,KAAK,WAAW;AAAA,MAClC,MAAM;AAAA,MACN,kBAAkB;AAAA,MAClB,MAAM;AAAA,QACL,GAAG;AAAA,QACH;AAAA,MACD;AAAA,IACD,CAAC;AAAA,EACF;AAAA,EAEA,MAAM,uBAAsC;AAK3C,QAAI;AACH,YAAM,WAAW,uBAAuB,IAAI,KAAK,IAAI;AAErD,UAAI,UAAU;AAEb,cAAM,SAAS,KAAK,sCAAsC;AAAA,UACzD,SAAS;AAAA,QACV,CAAC;AACD,cAAM,SAAS,KAAK,sCAAsC;AAAA,UACzD,SAAS;AAAA,QACV,CAAC;AAED;AAAA,MACD;AAEA,YAAM,SAAS,MAAM,KAAK,KAAK,iBAAiB;AAChD,YAAM,OAAO,KAAK,sCAAsC;AAAA,QACvD,SAAS;AAAA,MACV,CAAC;AACD,6BAAuB,IAAI,KAAK,MAAM,MAAM;AAAA,IAC7C,SAAS,OAAO;AACf,cAAQ,KAAK,6DAA6D,EAAE,MAAM,CAAC;AAAA,IACpF;AAAA,EACD;AACD;","names":[]}
@@ -22,13 +22,13 @@ export type BaseAuditSummary = {
22
22
  /** True if the audit stopped early after hitting `failedElementLimit`. */
23
23
  reachedFailedElementLimit: boolean;
24
24
  /**
25
- * True if the audit returned early because `timeout` elapsed. The results
26
- * are whatever had been gathered when the deadline hit.
25
+ * True if the audit returned early because `timeout` elapsed. The results are whatever had been
26
+ * gathered when the deadline hit.
27
27
  */
28
28
  timedOut: boolean;
29
29
  /**
30
- * Why the tab session ended, or `null` when this consumer disconnected
31
- * itself (element limit, failed-element limit, or timeout).
30
+ * Why the tab session ended, or `null` when this consumer disconnected itself (element limit,
31
+ * failed-element limit, or timeout).
32
32
  */
33
33
  sessionEnd: SessionEndReason | null;
34
34
  };
@@ -43,8 +43,7 @@ export type AuditSelfDisconnect = {
43
43
  clear(): void;
44
44
  };
45
45
  /**
46
- * Arms an optional self-disconnecting timeout and tracks whether this
47
- * consumer initiated its own disconnect, so `onSessionEnd` can tell that
48
- * apart from an external end.
46
+ * Arms an optional self-disconnecting timeout and tracks whether this consumer initiated its own
47
+ * disconnect, so `onSessionEnd` can tell that apart from an external end.
49
48
  */
50
49
  export declare function createAuditSelfDisconnect(): AuditSelfDisconnect;
@@ -1,7 +1,7 @@
1
1
  /**
2
- * IMPORTANT: Functions in this file are serialized and injected into the audited page, so they
3
- * must not reference any symbols outside their own scope. This also means they cannot share code,
4
- * so any common logic (like descending into shadow roots) must be duplicated.
2
+ * IMPORTANT: Functions in this file are serialized and injected into the audited page, so they must
3
+ * not reference any symbols outside their own scope. This also means they cannot share code, so any
4
+ * common logic (like descending into shadow roots) must be duplicated.
5
5
  */
6
6
  import type { Rect } from "./adaptor";
7
7
  import type { StyleSnapshot } from "./types";
@@ -30,9 +30,9 @@ export type ActiveElementBase = {
30
30
  index: number | null;
31
31
  isBody: boolean;
32
32
  /**
33
- * The focused element is an <iframe>. Tabbing into a frame passes focus into
34
- * its embedded document, but the parent's document.activeElement is the frame
35
- * element itself, so the loop uses this to skip the frame as a tab stop.
33
+ * The focused element is an <iframe>. Tabbing into a frame passes focus into its embedded
34
+ * document, but the parent's document.activeElement is the frame element itself, so the loop uses
35
+ * this to skip the frame as a tab stop.
36
36
  */
37
37
  isIframe: boolean;
38
38
  html: string;
@@ -40,9 +40,8 @@ export type ActiveElementBase = {
40
40
  rect: Rect;
41
41
  };
42
42
  /**
43
- * Read the currently focused element (descending open shadow roots), returning
44
- * its marker index, opening-tag HTML (marker attribute stripped), focus-relevant
45
- * styles, and page-relative rect.
43
+ * Read the currently focused element (descending open shadow roots), returning its marker index,
44
+ * opening-tag HTML (marker attribute stripped), focus-relevant styles, and page-relative rect.
46
45
  */
47
46
  export declare function probeActiveElementScript(props: string[], markerAttr: string): ActiveElementBase;
48
47
  export declare function activeElementHandleScript(): Element | null;
@@ -52,8 +51,8 @@ export declare function pageDimensionsScript(): {
52
51
  };
53
52
  export declare function elementRectScript(el: Element | null): Rect;
54
53
  /**
55
- * Whether something unrelated (e.g. a fixed cookie banner) covers the element's centre point, so
56
- * a screenshot of the element's box would show the covering element instead.
54
+ * Whether something unrelated (e.g. a fixed cookie banner) covers the element's centre point, so a
55
+ * screenshot of the element's box would show the covering element instead.
57
56
  */
58
57
  export declare function isCenterObscuredScript(el: Element | null): boolean;
59
58
  /**
@@ -61,26 +60,18 @@ export declare function isCenterObscuredScript(el: Element | null): boolean;
61
60
  * afterwards isn't captured mid-animation.
62
61
  */
63
62
  export declare function scrollToCenterScript(el: Element | null): void;
64
- /**
65
- * Blur (unfocus) an element
66
- */
67
63
  export declare function blurScript(el: Element | null): void;
68
- /**
69
- * Focus an element
70
- */
71
64
  export declare function focusScript(el: Element | null): void;
72
65
  /**
73
- * Read focus-relevant computed styles from a given element. Unlike
74
- * `probeActiveElementScript`, this does not use `document.activeElement`, so it
75
- * still works after the node has been blurred.
66
+ * Read focus-relevant computed styles from a given element. Unlike `probeActiveElementScript`, this
67
+ * does not use `document.activeElement`, so it still works after the node has been blurred.
76
68
  */
77
69
  export declare function elementStylesScript(el: Element | null, props: string[]): StyleSnapshot;
78
70
  /**
79
- * Remove the marker attribute from all elements, blur the active element, and
80
- * scroll back to the top. This is a best-effort attempt to reset state between
81
- * audits: the tab loop scrolls later elements into view as it walks the page
82
- * (see scrollToCenterScript), and nothing else undoes that once the session
83
- * ends.
71
+ * Remove the marker attribute from all elements, blur the active element, and scroll back to the
72
+ * top. This is a best-effort attempt to reset state between audits: the tab loop scrolls later
73
+ * elements into view as it walks the page (see scrollToCenterScript), and nothing else undoes that
74
+ * once the session ends.
84
75
  */
85
76
  export declare function clearMarkersScript(markerAttr: string): void;
86
77
  export type MeasureObscuringResult = {
@@ -93,9 +84,9 @@ export type MeasureObscuringResult = {
93
84
  hasObscurer: boolean;
94
85
  };
95
86
  /**
96
- * Hit-test whether author-created content entirely hides the focused element.
97
- * Marks a single covering element with `obscurerAttr` when containment confirms
98
- * a full cover, so the host can resolve its selector via getSelector.
87
+ * Hit-test whether author-created content entirely hides the focused element. Marks a single
88
+ * covering element with `obscurerAttr` when containment confirms a full cover, so the host can
89
+ * resolve its selector via getSelector.
99
90
  */
100
91
  export declare function measureObscuringScript(el: Element | null, obscurerAttr: string): MeasureObscuringResult;
101
92
  /** Return the element marked as the obscurer, if any. */
@@ -116,8 +107,8 @@ declare global {
116
107
  }
117
108
  }
118
109
  /**
119
- * Install once: wrap window.open (record + inert stub), capture-phase submit
120
- * (record + preventDefault), track focusin and soft URL changes.
110
+ * Install once: wrap window.open (record + inert stub), capture-phase submit (record +
111
+ * preventDefault), track focusin and soft URL changes.
121
112
  */
122
113
  export declare function installContextObserverScript(): void;
123
114
  export type DrainContextObserverResult = {
@@ -132,20 +123,23 @@ export type DrainContextObserverResult = {
132
123
  hasAttributed: boolean;
133
124
  };
134
125
  /**
135
- * Drain observer signals for the current tab stop and reset per-stop flags.
136
- * Classification of focus removal / redirection uses the post-Tab settle
137
- * activeElement and the focusin history from this stop only.
126
+ * Drain observer signals for the current tab stop and reset per-stop flags. Classification of focus
127
+ * removal / redirection uses the post-Tab settle activeElement and the focusin history from this
128
+ * stop only.
138
129
  */
139
130
  export declare function drainContextObserverScript(markerAttr: string): DrainContextObserverResult;
140
- /** Return the element marked as attributed by drainContextObserverScript (the
141
- * element that received a focus event this stop and then lost it), if any. */
131
+ /**
132
+ * Return the element marked as attributed by drainContextObserverScript (the element that received
133
+ * a focus event this stop and then lost it), if any.
134
+ */
142
135
  export declare function attributedHandleScript(): Element | null;
143
136
  export declare function clearAttributedScript(): void;
144
137
  /**
145
- * Drop focusin history accumulated after drain (e.g. pixel-diff blur/refocus)
146
- * so the next tab stop does not mis-attribute F55 or redirects.
138
+ * Drop focusin history accumulated after drain (e.g. pixel-diff blur/refocus) so the next tab stop
139
+ * does not mis-attribute F55 or redirects.
147
140
  */
148
141
  export declare function clearContextFocusInsScript(): void;
149
142
  /** Current location.href for navigation comparison in the host. */
150
143
  export declare function locationHrefScript(): string;
144
+ export declare function hasFocusScript(): boolean;
151
145
  export {};
@@ -4,7 +4,7 @@ export type CaptureScreenshotOptions = {
4
4
  };
5
5
  /**
6
6
  * Screenshot the element `handle` currently refers to, clipped to its box plus
7
- * `screenshotClipBuffer` padding. The caller owns `handle` and must dispose it
8
- * after this promise settles.
7
+ * `screenshotClipBuffer` padding. The caller owns `handle` and must dispose it after this promise
8
+ * settles.
9
9
  */
10
10
  export declare function captureScreenshot(adaptor: BrowserAdaptor, options: CaptureScreenshotOptions, handle: ElementRef): Promise<Uint8Array>;
package/dist/clip.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { Rect } from "./adaptor";
2
2
  /**
3
- * A screenshot clip framing `rect` with up to `buffer` pixels of padding on each side, shrunk
4
- * where the page edges leave less room. The padding allows for indicators drawn just outside
5
- * the element box (outlines, shadows) and for anti-aliasing.
3
+ * A screenshot clip framing `rect` with up to `buffer` pixels of padding on each side, shrunk where
4
+ * the page edges leave less room. The padding allows for indicators drawn just outside the element
5
+ * box (outlines, shadows) and for anti-aliasing.
6
6
  */
7
7
  export declare function bufferedClip(rect: Rect, pageWidth: number, pageHeight: number, buffer: number): Rect;
@@ -1,7 +1,6 @@
1
1
  /**
2
- * Whether focus destination Y is inside the subtree of intended focus X
3
- * (descendant or ancestor), which is legitimate delegation rather than theft.
4
- * Walks across open shadow roots so focusing into a host's shadow content is
5
- * same-subtree, not theft.
2
+ * Whether focus destination Y is inside the subtree of intended focus X (descendant or ancestor),
3
+ * which is legitimate delegation rather than theft. Walks across open shadow roots so focusing into
4
+ * a host's shadow content is same-subtree, not theft.
6
5
  */
7
6
  export declare function isSameFocusSubtree(intended: Element, settled: Element): boolean;
@@ -5,7 +5,7 @@
5
5
  *
6
6
  * Mirrors axe-core's approach: each level contributes the fewest features that discriminate the
7
7
  * element (a unique id, the rarest single class, or an :nth-of-type position), and the walk stops
8
- * as soon as the joined selector matches exactly one element. Selectors stay short because of
9
- * that early stop, not because of truncation; the length budget is only a backstop.
8
+ * as soon as the joined selector matches exactly one element. Selectors stay short because of that
9
+ * early stop, not because of truncation; the length budget is only a backstop.
10
10
  */
11
11
  export declare function getSelector(root: Node | null): string;
package/dist/index.d.ts CHANGED
@@ -2,7 +2,7 @@ export type { BrowserAdaptor, ElementRef, Rect } from "./adaptor";
2
2
  export type { AuditSelfDisconnect, BaseAuditOptions, BaseAuditSummary, } from "./audit-scaffold";
3
3
  export { createAuditSelfDisconnect, DEFAULT_ELEMENT_LIMIT, DEFAULT_FAILED_ELEMENT_LIMIT, DEFAULT_SCREENSHOT_SETTLE_DELAY, DEFAULT_TIMEOUT, } from "./audit-scaffold";
4
4
  export type { ActiveElementBase, BaselinePayload } from "./browser-scripts";
5
- export { activeElementHandleScript, baselineScript, blurScript, clearMarkersScript, elementRectScript, elementStylesScript, focusScript, isCenterObscuredScript, pageDimensionsScript, probeActiveElementScript, scrollToCenterScript, } from "./browser-scripts";
5
+ export { activeElementHandleScript, baselineScript, blurScript, clearMarkersScript, elementRectScript, elementStylesScript, focusScript, hasFocusScript, isCenterObscuredScript, pageDimensionsScript, probeActiveElementScript, scrollToCenterScript, } from "./browser-scripts";
6
6
  export { bufferedClip } from "./clip";
7
7
  export { FOCUS_STYLE_PROPERTIES } from "./focus-style";
8
8
  export { getSelector } from "./get-selector";