UNPKG

4.82 kBPlain TextView Raw
1/**
2 * @license
3 * Copyright 2023 Google Inc.
4 * SPDX-License-Identifier: Apache-2.0
5 */
6
7import type PuppeteerUtil from '../injected/injected.js';
8import {assert} from '../util/assert.js';
9import {interpolateFunction, stringifyFunction} from '../util/Function.js';
10
11import {
12 QueryHandler,
13 type QuerySelector,
14 type QuerySelectorAll,
15} from './QueryHandler.js';
16import {scriptInjector} from './ScriptInjector.js';
17
18/**
19 * @public
20 */
21export interface CustomQueryHandler {
22 /**
23 * Searches for a {@link https://developer.mozilla.org/en-US/docs/Web/API/Node | Node} matching the given `selector` from {@link https://developer.mozilla.org/en-US/docs/Web/API/Node | node}.
24 */
25 queryOne?: (node: Node, selector: string) => Node | null;
26 /**
27 * Searches for some {@link https://developer.mozilla.org/en-US/docs/Web/API/Node | Nodes} matching the given `selector` from {@link https://developer.mozilla.org/en-US/docs/Web/API/Node | node}.
28 */
29 queryAll?: (node: Node, selector: string) => Iterable<Node>;
30}
31
32/**
33 * The registry of {@link CustomQueryHandler | custom query handlers}.
34 *
35 * @example
36 *
37 * ```ts
38 * Puppeteer.customQueryHandlers.register('lit', { … });
39 * const aHandle = await page.$('lit/…');
40 * ```
41 *
42 * @internal
43 */
44export class CustomQueryHandlerRegistry {
45 #handlers = new Map<
46 string,
47 [registerScript: string, Handler: typeof QueryHandler]
48 >();
49
50 get(name: string): typeof QueryHandler | undefined {
51 const handler = this.#handlers.get(name);
52 return handler ? handler[1] : undefined;
53 }
54
55 /**
56 * Registers a {@link CustomQueryHandler | custom query handler}.
57 *
58 * @remarks
59 * After registration, the handler can be used everywhere where a selector is
60 * expected by prepending the selection string with `<name>/`. The name is
61 * only allowed to consist of lower- and upper case latin letters.
62 *
63 * @example
64 *
65 * ```ts
66 * Puppeteer.customQueryHandlers.register('lit', { … });
67 * const aHandle = await page.$('lit/…');
68 * ```
69 *
70 * @param name - Name to register under.
71 * @param queryHandler - {@link CustomQueryHandler | Custom query handler} to
72 * register.
73 */
74 register(name: string, handler: CustomQueryHandler): void {
75 assert(
76 !this.#handlers.has(name),
77 `Cannot register over existing handler: ${name}`,
78 );
79 assert(
80 /^[a-zA-Z]+$/.test(name),
81 `Custom query handler names may only contain [a-zA-Z]`,
82 );
83 assert(
84 handler.queryAll || handler.queryOne,
85 `At least one query method must be implemented.`,
86 );
87
88 const Handler = class extends QueryHandler {
89 static override querySelectorAll: QuerySelectorAll = interpolateFunction(
90 (node, selector, PuppeteerUtil) => {
91 return PuppeteerUtil.customQuerySelectors
92 .get(PLACEHOLDER('name'))!
93 .querySelectorAll(node, selector);
94 },
95 {name: JSON.stringify(name)},
96 );
97 static override querySelector: QuerySelector = interpolateFunction(
98 (node, selector, PuppeteerUtil) => {
99 return PuppeteerUtil.customQuerySelectors
100 .get(PLACEHOLDER('name'))!
101 .querySelector(node, selector);
102 },
103 {name: JSON.stringify(name)},
104 );
105 };
106 const registerScript = interpolateFunction(
107 (PuppeteerUtil: PuppeteerUtil) => {
108 PuppeteerUtil.customQuerySelectors.register(PLACEHOLDER('name'), {
109 queryAll: PLACEHOLDER('queryAll'),
110 queryOne: PLACEHOLDER('queryOne'),
111 });
112 },
113 {
114 name: JSON.stringify(name),
115 queryAll: handler.queryAll
116 ? stringifyFunction(handler.queryAll)
117 : String(undefined),
118 queryOne: handler.queryOne
119 ? stringifyFunction(handler.queryOne)
120 : String(undefined),
121 },
122 ).toString();
123
124 this.#handlers.set(name, [registerScript, Handler]);
125 scriptInjector.append(registerScript);
126 }
127
128 /**
129 * Unregisters the {@link CustomQueryHandler | custom query handler} for the
130 * given name.
131 *
132 * @throws `Error` if there is no handler under the given name.
133 */
134 unregister(name: string): void {
135 const handler = this.#handlers.get(name);
136 if (!handler) {
137 throw new Error(`Cannot unregister unknown handler: ${name}`);
138 }
139 scriptInjector.pop(handler[0]);
140 this.#handlers.delete(name);
141 }
142
143 /**
144 * Gets the names of all {@link CustomQueryHandler | custom query handlers}.
145 */
146 names(): string[] {
147 return [...this.#handlers.keys()];
148 }
149
150 /**
151 * Unregisters all custom query handlers.
152 */
153 clear(): void {
154 for (const [registerScript] of this.#handlers) {
155 scriptInjector.pop(registerScript);
156 }
157 this.#handlers.clear();
158 }
159}
160
161/**
162 * @internal
163 */
164export const customQueryHandlers = new CustomQueryHandlerRegistry();