@odg/chemical-x 2.1.3 → 2.3.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.
@@ -0,0 +1,240 @@
1
+ ## Chemical-X Crawler API - Pages, Handlers, BrowserManager
2
+
3
+ Arquitetura para automação web com abstração sobre Puppeteer/Playwright. Requer driver de browser instalado separadamente.
4
+
5
+ ```typescript
6
+ import { BrowserManager, BasePage, BaseHandler, Browser, Context, Page, Container } from "@odg/chemical-x";
7
+ ```
8
+
9
+ ---
10
+
11
+ ### BrowserManager
12
+
13
+ Orquestrador central que cria instâncias de Browser e Context via factories injetadas pelo consumidor.
14
+
15
+ **Assinatura:**
16
+
17
+ ```typescript
18
+ class BrowserManager<BrowserClassEngine, ContextClassEngine, PageClassEngine> {
19
+ constructor(
20
+ $newBrowser: CreateBrowserFactoryType<BrowserClassEngine, ContextClassEngine, PageClassEngine>,
21
+ $newContext: CreateContextFactoryType<ContextClassEngine, PageClassEngine>,
22
+ $newPage: CreatePageFactoryType<PageClassEngine>,
23
+ )
24
+
25
+ async newBrowser(
26
+ browser: () => Promise<BrowserClassEngine>,
27
+ ): Promise<BrowserChemicalXInterface<...> & BrowserClassEngine>
28
+
29
+ async newPersistentContext(
30
+ context: () => Promise<ContextClassEngine>,
31
+ ): Promise<ContextChemicalXInterface<...> & ContextClassEngine>
32
+ }
33
+ ```
34
+
35
+ **Responsabilidades:**
36
+
37
+ - Cria instâncias de `Browser` via `newBrowser()` — recebe factory que retorna engine do browser
38
+ - Cria contextos persistentes via `newPersistentContext()` — recebe factory que retorna engine do contexto
39
+ - **Não auto-seleciona driver**: consumer configura Puppeteer ou Playwright via factory injetada
40
+ - Retorna objetos que combinam interface Chemical-X com o engine nativo (via `@getterAccess` proxy)
41
+
42
+ **Lifecycle:**
43
+
44
+ 1. Consumer instancia `BrowserManager` com factories (normalmente via Container/DI)
45
+ 2. `newBrowser(() => puppeteer.launch())` → retorna `Browser` com proxy sobre Puppeteer
46
+ 3. `browser.newContext(options?)` → retorna `Context` com proxy
47
+ 4. `context.newPage()` → retorna `Page` com proxy
48
+
49
+ ---
50
+
51
+ ### Browser, Context, Page (Wrappers)
52
+
53
+ Wrappers com `@getterAccess` que delegam acesso ao engine nativo de forma transparente.
54
+
55
+ **Browser:**
56
+
57
+ ```typescript
58
+ @ODGDecorators.getterAccess()
59
+ class Browser<BrowserClassEngine, ContextClassEngine, PageClassEngine>
60
+ implements GetterAccessInterface, BrowserChemicalXInterface<...> {
61
+
62
+ $browserInstance: BrowserClassEngine;
63
+ async defaultContextOptions(): Promise<ContextOptionsLibraryInterface>;
64
+ async newContext(options?): Promise<ContextChemicalXInterface<...> & ContextClassEngine>;
65
+ contexts(): Array<ContextChemicalXInterface<...> & ContextClassEngine>;
66
+ __get(key: PropertyKey): unknown;
67
+ }
68
+ ```
69
+
70
+ **Context:**
71
+
72
+ ```typescript
73
+ @ODGDecorators.getterAccess()
74
+ class Context<ContextClassEngine, PageClassEngine>
75
+ implements GetterAccessInterface, ContextChemicalXInterface<...> {
76
+
77
+ $contextInstance: ContextClassEngine;
78
+ async defaultPageOptions(): Promise<PageOptionsLibraryInterface>;
79
+ async newPage(options?): Promise<PageChemicalXInterface<...> & PageClassEngine>;
80
+ pages(): Array<PageChemicalXInterface<...> & PageClassEngine>;
81
+ __get(key: PropertyKey): unknown;
82
+ }
83
+ ```
84
+
85
+ **Page:**
86
+
87
+ ```typescript
88
+ @ODGDecorators.getterAccess()
89
+ class Page<ContextClassEngine, PageClassEngine>
90
+ implements GetterAccessInterface, PageChemicalXInterface<...> {
91
+
92
+ $pageInstance: PageClassEngine;
93
+ context(): ContextChemicalXInterface<...> & ContextClassEngine;
94
+ __get(key: PropertyKey): unknown;
95
+ }
96
+ ```
97
+
98
+ Todos usam `@getterAccess` para delegar acessos de propriedade/método ao engine subjacente. Isso permite chamar métodos nativos do Puppeteer/Playwright diretamente no wrapper.
99
+
100
+ ---
101
+
102
+ ### BasePage (Abstract)
103
+
104
+ Define uma página por **intenção**, não por URL. Implementa `AttemptableInterface` com lifecycle hooks para retry via `@attemptableFlow`.
105
+
106
+ **Assinatura:**
107
+
108
+ ```typescript
109
+ abstract class BasePage<PageClassEngine> implements PageInterface {
110
+ currentAttempt: number = 0;
111
+ page?: PageClassEngine;
112
+
113
+ abstract readonly $s?: SelectorType; // Seletor principal
114
+ abstract readonly $$s?: Record<string | number | symbol, SelectorType>; // Mapa de seletores
115
+
116
+ setPage(page: PageClassEngine): this;
117
+
118
+ // Obrigatórios (abstract)
119
+ abstract execute(): Promise<void>; // Ação principal da página
120
+ abstract attempt(): Promise<number>; // Retorna número de tentativas
121
+
122
+ // Hooks opcionais (lifecycle do @attemptableFlow)
123
+ success?(): Promise<void>;
124
+ failure?(exception: Exception): Promise<void>;
125
+ retrying?(exception: Exception, attempt: number): Promise<RetryAction>;
126
+ finish?(exception?: Exception): Promise<void>;
127
+ sleep?(): Promise<number>;
128
+ }
129
+ ```
130
+
131
+ **Page Intent Design:**
132
+
133
+ > Pages agrupam por **INTENÇÃO**, não por URL. Uma mesma URL pode ter múltiplas Pages.
134
+
135
+ Exemplos:
136
+ - `LoginPage` (intenção: autenticar) — URL: `/login`
137
+ - `LoginVerificationPage` (intenção: verificar se login OK) — URL: `/login` ou `/dashboard`
138
+ - `HomeContentPage` (intenção: extrair conteúdo) — URL: `/`
139
+ - `HomeAdPage` (intenção: extrair anúncios) — URL: `/`
140
+
141
+ **Lifecycle:**
142
+
143
+ 1. `setPage(page)` — injeta instância da page do browser
144
+ 2. `execute()` — executa ação (navegar, preencher, clicar)
145
+ 3. Se falha → `retrying(exception, attempt)` decide retry ou não
146
+ 4. Se sucesso → `success()`
147
+ 5. Se falha final → `failure(exception)`
148
+ 6. Sempre → `finish(exception?)`
149
+
150
+ ---
151
+
152
+ ### BaseHandler (Abstract)
153
+
154
+ Valida transições e resultados de páginas. **Handlers validam, não executam ações.**
155
+
156
+ **Assinatura:**
157
+
158
+ ```typescript
159
+ abstract class BaseHandler<PageClassEngine> implements HandlerInterface {
160
+ currentAttempt: number = 0;
161
+ page?: PageClassEngine;
162
+
163
+ abstract readonly $$s: Record<string | number | symbol, SelectorType>;
164
+
165
+ setPage(page: PageClassEngine): this;
166
+
167
+ // Concrete
168
+ async execute(): Promise<void>;
169
+ async successSolution(): Promise<HandlerSolutionType>; // Retorna RetryAction.Resolve
170
+
171
+ // Obrigatórios (abstract)
172
+ abstract waitForHandler(): Promise<HandlerFunction>; // Retorna Exception ou solution function
173
+ abstract attempt(): Promise<number>;
174
+
175
+ // Hooks opcionais
176
+ success?(): Promise<void>;
177
+ failure?(exception: Exception): Promise<void>;
178
+ retrying?(exception: Exception, attempt: number): Promise<RetryAction>;
179
+ finish?(exception?: Exception): Promise<void>;
180
+ sleep?(): Promise<number>;
181
+ }
182
+ ```
183
+
184
+ **Tipos de retorno de Handler:**
185
+
186
+ ```typescript
187
+ type HandlerSolutionType = Exception | Exclude<RetryAction, RetryAction.Default | RetryAction.Throw>;
188
+ type HandlerFunction = Exception | (() => Promise<HandlerSolutionType>);
189
+ ```
190
+
191
+ **Handler Pattern:**
192
+
193
+ > "Pages executam. Handlers validam. Handlers declaram Solution ou lançam Exception."
194
+
195
+ - `waitForHandler()` retorna `Exception` → falha detectada
196
+ - `waitForHandler()` retorna `() => Promise<HandlerSolutionType>` → solução disponível
197
+ - Solution pode ser: `RetryAction.Resolve` (sucesso), `RetryAction.Retry` (tentar novamente)
198
+ - Handler **NUNCA** interage com a page diretamente
199
+ - Handler **NUNCA** chama outras Pages para resolver problemas
200
+ - Handler **NUNCA** falha silenciosamente
201
+
202
+ ---
203
+
204
+ ### Container / DI
205
+
206
+ Chemical-X usa Inversify para DI. `@ODGDecorators.injectable(name)` registra metadata; consumer deve chamar `Container.loadModule()`.
207
+
208
+ **Regras:**
209
+
210
+ 1. `Container.loadModule()` **obrigatório** antes de executar qualquer Page/Handler registrado
211
+ 2. DI binding é **responsabilidade do consumidor** — Chemical-X não auto-wira
212
+ 3. `Container.getOptional(name)` retorna `undefined` se serviço não registrado (vs `get()` que lança)
213
+ 4. Puppeteer/Playwright deve ser injetado via Container ou passado ao `BrowserManager` constructor
214
+
215
+ ---
216
+
217
+ ### Workflow Example
218
+
219
+ Fluxo completo: login em site com tratamento de 2FA.
220
+
221
+ ```
222
+ 1. Create LoginPage (intent: autenticar)
223
+ → setPage(page) → execute() (navegar, preencher credenciais, clicar login)
224
+
225
+ 2. Create LoginHandler (intent: validar resultado do login)
226
+ → setPage(page) → waitForHandler()
227
+ → Detecta 2FA required → return () => new TwoFaPage() as Solution
228
+ → Detecta login error → return new InvalidCredentialsException()
229
+ → Detecta sucesso → return successSolution() (RetryAction.Resolve)
230
+
231
+ 3. Se Solution = TwoFaPage:
232
+ → Create TwoFaPage (intent: resolver 2FA)
233
+ → setPage(page) → execute() (preencher código 2FA)
234
+
235
+ 4. Create DashboardHandler (intent: verificar que chegou no dashboard)
236
+ → setPage(page) → waitForHandler()
237
+ → Verifica URL/conteúdo → return successSolution()
238
+ ```
239
+
240
+ See also: `tests/vitest/Handlers/` e `tests/vitest/Pages/` para exemplos reais de implementação.
@@ -0,0 +1,258 @@
1
+ ## Chemical-X Decorators - @ODGDecorators.attemptableFlow, @ODGDecorators.getterAccess, @ODGDecorators.injectable
2
+
3
+ DSL baseada em decorators para retry com lifecycle, interceptação de acesso e DI registration.
4
+
5
+ ```typescript
6
+ import { ODGDecorators } from "@odg/chemical-x";
7
+ ```
8
+
9
+ ---
10
+
11
+ ### `@ODGDecorators.attemptableFlow()`
12
+
13
+ Decorator de classe que adiciona retry com lifecycle hooks completo. Envolve `execute()` com lógica de retentativa baseada em `AttemptableInterface`.
14
+
15
+ **Aplicação:**
16
+
17
+ ```typescript
18
+ @ODGDecorators.attemptableFlow()
19
+ class MyPage extends BasePage<PageEngine> {
20
+ // ...
21
+ }
22
+ ```
23
+
24
+ **Lifecycle Hooks:**
25
+
26
+ | Hook | Obrigatório | Quando chamado |
27
+ |---|---|---|
28
+ | `execute()` | Sim | Ação principal; retentada até `attempt()` vezes |
29
+ | `attempt()` | Sim | Retorna número máximo de tentativas |
30
+ | `sleep()` | Não | Retorna ms entre tentativas (se definido) |
31
+ | `success()` | Não | Chamado quando `execute()` sucede |
32
+ | `failure(exception)` | Não | Chamado quando todas tentativas falham; recebe a última exceção |
33
+ | `retrying(exception, attempt)` | Não | Chamado antes de cada retry; retorna `RetryAction` para decidir ação |
34
+ | `finish(exception?)` | Não | Chamado sempre ao final (sucesso ou falha); recebe exceção se houve |
35
+
36
+ **Propriedade:**
37
+
38
+ - `currentAttempt: number` — número da tentativa atual (atualizado automaticamente pelo decorator)
39
+
40
+ **Fluxo de Execução:**
41
+
42
+ ```
43
+ execute() chamado
44
+ ├─ Sucesso → success() → finish()
45
+ └─ Falha → retrying(exception, attempt)?
46
+ ├─ RetryAction.Retry → execute() novamente
47
+ ├─ RetryAction.Throw → failure(exception) → finish(exception)
48
+ ├─ RetryAction.Resolve → finish() (sem exceção, resolve undefined)
49
+ └─ RetryAction.Default → se attempts restam → sleep() → execute()
50
+ └─ se esgotou → failure(exception) → finish(exception)
51
+ ```
52
+
53
+ **Exemplo:**
54
+
55
+ ```typescript
56
+ @ODGDecorators.attemptableFlow()
57
+ class LoginPage extends BasePage<PuppeteerPage> {
58
+ readonly $s = "form#login";
59
+ readonly $$s = {
60
+ username: "input[name=username]",
61
+ password: "input[name=password]",
62
+ submit: "button[type=submit]",
63
+ };
64
+
65
+ async attempt(): Promise<number> {
66
+ return 3;
67
+ }
68
+
69
+ async execute(): Promise<void> {
70
+ await this.page!.type(this.$$s.username, "user@example.com");
71
+ await this.page!.type(this.$$s.password, "password123");
72
+ await this.page!.click(this.$$s.submit);
73
+ }
74
+
75
+ async sleep(): Promise<number> {
76
+ return 2000; // 2s entre tentativas
77
+ }
78
+
79
+ async success(): Promise<void> {
80
+ console.log("Login successful");
81
+ }
82
+
83
+ async failure(exception: Exception): Promise<void> {
84
+ console.error("Login failed after all attempts:", exception.message);
85
+ }
86
+
87
+ async retrying(exception: Exception, attempt: number): Promise<RetryAction> {
88
+ if (exception instanceof BrowserException) return RetryAction.Throw;
89
+ return RetryAction.Default;
90
+ }
91
+ }
92
+ ```
93
+
94
+ See also: `tests/vitest/Pages/` para exemplos de Pages com `@attemptableFlow`.
95
+
96
+ ---
97
+
98
+ ### `@ODGDecorators.getterAccess()`
99
+
100
+ Decorator de classe que cria um Proxy interceptando **todo** acesso a propriedade/método via `__get(key, value)`.
101
+
102
+ **Aplicação:**
103
+
104
+ ```typescript
105
+ @ODGDecorators.getterAccess()
106
+ class MyWrapper implements GetterAccessInterface {
107
+ __get(key: PropertyKey, value: unknown): unknown {
108
+ // Intercepta todo acesso
109
+ return value;
110
+ }
111
+ }
112
+ ```
113
+
114
+ **Comportamento do Proxy:**
115
+
116
+ - **Todo** acesso a propriedade (leitura) passa por `__get(key, value)` onde:
117
+ - `key` = nome da propriedade acessada
118
+ - `value` = valor original da propriedade (se existir)
119
+ - Permite: delegação transparente ao engine, validação, lazy-load, tracking, logging
120
+
121
+ **Uso interno no Chemical-X:**
122
+
123
+ `Browser`, `Context` e `Page` usam `@getterAccess` para delegar ao engine nativo:
124
+
125
+ ```typescript
126
+ @ODGDecorators.getterAccess()
127
+ class Browser<BrowserClassEngine, ...> implements GetterAccessInterface {
128
+ $browserInstance: BrowserClassEngine;
129
+
130
+ __get(key: PropertyKey): unknown {
131
+ // Delega ao engine nativo (Puppeteer/Playwright)
132
+ return (this.$browserInstance as Record<PropertyKey, unknown>)[key];
133
+ }
134
+ }
135
+ ```
136
+
137
+ Isso permite:
138
+ ```typescript
139
+ const browser = await manager.newBrowser(() => puppeteer.launch());
140
+ // Acessa métodos nativos do Puppeteer diretamente no wrapper:
141
+ await browser.close(); // Delegado via __get → puppeteerBrowser.close()
142
+ ```
143
+
144
+ See also: `tests/vitest/puppeteer/` e `tests/vitest/playwright/` para exemplos de uso com drivers reais.
145
+
146
+ ---
147
+
148
+ ### `@ODGDecorators.injectable(name, scope?)`
149
+
150
+ Registra classe no Container (Inversify) com `@injectable()` e `@injectFromHierarchy()`.
151
+
152
+ **Aplicação:**
153
+
154
+ ```typescript
155
+ @ODGDecorators.injectable("LoginPage")
156
+ @ODGDecorators.attemptableFlow()
157
+ class LoginPage extends BasePage<PageEngine> {
158
+ // ...
159
+ }
160
+ ```
161
+
162
+ **Regras:**
163
+
164
+ 1. Define metadata para DI — **não registra automaticamente no Container**
165
+ 2. Consumer **deve** chamar `Container.loadModule()` para efetivar os bindings
166
+ 3. Combinado com `@attemptableFlow` em Pages e Handlers
167
+ 4. O `name` é usado como identificador do binding no Container
168
+ 5. Deve sempre ficar a cima de todos os outros decorators (ex: `@attemptableFlow`) para garantir que a classe seja registrada apos de ser processada por outros decorators
169
+
170
+ ---
171
+
172
+ ### `@registerListener(eventName, containerName, options)`
173
+
174
+ Registra listener de eventos em Container EventEmitter via metadata.
175
+
176
+ **Aplicação:**
177
+
178
+ ```typescript
179
+ @ODGDecorators.registerListener("page:loaded", "PageEventEmitter", { once: true })
180
+ class PageLoadedListener {
181
+ // ...
182
+ }
183
+ ```
184
+
185
+ ---
186
+
187
+ ### Comparison - When to Use
188
+
189
+ | Cenário | Ferramenta | Motivo |
190
+ |---|---|---|
191
+ | Retry simples de callback | `retry()` | Função isolada, sem lifecycle, sem estado |
192
+ | Retry de classe inteira com hooks | `@ODGDecorators.attemptableFlow` | Lifecycle completo: attempt, success, failure, retrying, finish, sleep |
193
+ | Interceptar propriedades | `@ODGDecorators.getterAccess` | Proxy transparente para delegação/validação |
194
+ | Registrar no Container | `@ODGDecorators.injectable` | DI via Inversify com hierarchy |
195
+ | Callback com timeout | `timeout()` | Sem retry, apenas limite de tempo |
196
+ | Classe retriable com timeout | `@ODGDecorators.attemptableFlow` + `timeout()` no `execute()` | Combine ambos para retry + timeout |
197
+
198
+ **Regra geral:**
199
+ - Use `retry()` quando quer retentar **uma função**
200
+ - Use `@ODGDecorators.attemptableFlow` quando quer retentar **um comportamento de classe** com estado e lifecycle
201
+
202
+ ---
203
+
204
+ ### Common Patterns
205
+
206
+ **@ODGDecorators.attemptableFlow com handler e solução:**
207
+
208
+ ```typescript
209
+ @ODGDecorators.injectable("LoginHandler")
210
+ @ODGDecorators.attemptableFlow()
211
+ class LoginHandler extends BaseHandler<PuppeteerPage> {
212
+ readonly $$s = {
213
+ errorMsg: ".error-message",
214
+ dashboard: "#dashboard",
215
+ twoFa: "#two-fa-form",
216
+ };
217
+
218
+ async attempt(): Promise<number> {
219
+ return 5;
220
+ }
221
+
222
+ async waitForHandler(): Promise<HandlerFunction> {
223
+ // Espera por um dos seletores aparecer na página
224
+ const found = await this.page!.waitForSelector(
225
+ [this.$$s.errorMsg, this.$$s.dashboard, this.$$s.twoFa].join(", "),
226
+ );
227
+
228
+ if (found?.matches(this.$$s.errorMsg)) {
229
+ return new BrowserException("Login failed");
230
+ }
231
+
232
+ if (found?.matches(this.$$s.twoFa)) {
233
+ return async () => RetryAction.Retry; // Indica que precisa retry (2FA page)
234
+ }
235
+
236
+ return async () => this.successSolution(); // RetryAction.Resolve
237
+ }
238
+ }
239
+ ```
240
+
241
+ **@ODGDecorators.getterAccess para wrapper customizado:**
242
+
243
+ ```typescript
244
+ @ODGDecorators.getterAccess()
245
+ class CachedPage implements GetterAccessInterface {
246
+ private cache = new Map<PropertyKey, unknown>();
247
+ private wrapped: SomeEngine;
248
+
249
+ __get(key: PropertyKey, value: unknown): unknown {
250
+ if (this.cache.has(key)) return this.cache.get(key);
251
+ const result = (this.wrapped as Record<PropertyKey, unknown>)[key];
252
+ this.cache.set(key, result);
253
+ return result;
254
+ }
255
+ }
256
+ ```
257
+
258
+ See also: `tests/vitest/Handlers/` e `tests/vitest/Listeners/` para exemplos reais.
@@ -0,0 +1,212 @@
1
+ ## Chemical-X Exceptions - Reference Guide
2
+
3
+ Referência completa de todas as exceções públicas. Todas estendem `Exception` de `@odg/exception`.
4
+
5
+ ```typescript
6
+ import {
7
+ BrowserException,
8
+ BrowserInstanceException,
9
+ RetryException,
10
+ TimeoutException,
11
+ InvalidArgumentException,
12
+ MoneyNotFoundException,
13
+ MoneyMultipleResultException,
14
+ } from "@odg/chemical-x";
15
+ ```
16
+
17
+ ---
18
+
19
+ ### Exception Reference
20
+
21
+ #### `BrowserException`
22
+
23
+ | | |
24
+ |---|---|
25
+ | **Trigger** | Falha em operação do browser em runtime (crash, perda de conexão, operação inválida) |
26
+ | **Quando** | Interações com Page/Browser via Crawler API (click, type, goto, etc.) |
27
+ | **Extends** | `Exception` (`@odg/exception`) |
28
+ | **Handling** | Catch e retry (via `@attemptableFlow`) ou fallback |
29
+
30
+ ```typescript
31
+ try {
32
+ await page.execute();
33
+ } catch (exception) {
34
+ if (exception instanceof BrowserException) {
35
+ // Browser crashed ou conexão perdida — retry ou fallback
36
+ }
37
+ }
38
+ ```
39
+
40
+ ---
41
+
42
+ #### `BrowserInstanceException`
43
+
44
+ | | |
45
+ |---|---|
46
+ | **Trigger** | Falha ao criar/inicializar instância do browser |
47
+ | **Quando** | `BrowserManager.newBrowser()` ou `BrowserManager.newPersistentContext()` |
48
+ | **Extends** | `BrowserException` |
49
+ | **Handling** | Verificar setup do driver (Puppeteer/Playwright instalado?), retry init |
50
+
51
+ ```typescript
52
+ try {
53
+ const browser = await manager.newBrowser(() => puppeteer.launch());
54
+ } catch (exception) {
55
+ if (exception instanceof BrowserInstanceException) {
56
+ // Driver não encontrado ou falha ao iniciar — verificar instalação
57
+ }
58
+ }
59
+ ```
60
+
61
+ ---
62
+
63
+ #### `RetryException`
64
+
65
+ | | |
66
+ |---|---|
67
+ | **Trigger** | Todas as tentativas de `retry()` esgotadas sem sucesso |
68
+ | **Quando** | `retry()` completa todos os `times` sem callback suceder |
69
+ | **Extends** | `Exception` (`@odg/exception`) |
70
+ | **Handling** | Fallback final ou propagar erro ao chamador |
71
+
72
+ ```typescript
73
+ try {
74
+ await retry({ times: 3, callback: async () => { /* ... */ } });
75
+ } catch (exception) {
76
+ if (exception instanceof RetryException) {
77
+ // Todas tentativas falharam — aplicar fallback ou logar erro final
78
+ }
79
+ }
80
+ ```
81
+
82
+ ---
83
+
84
+ #### `TimeoutException`
85
+
86
+ | | |
87
+ |---|---|
88
+ | **Trigger** | Operação excede o limite de tempo definido em `timeout()` |
89
+ | **Quando** | `timeout({ timeout: ms, callback })` quando callback não completa a tempo |
90
+ | **Extends** | `Exception` (`@odg/exception`) |
91
+ | **Handling** | Catch e tratar timeout; ajustar limite se operação legitimamente lenta |
92
+
93
+ ```typescript
94
+ try {
95
+ await timeout({
96
+ name: "loadPage",
97
+ timeout: 5000,
98
+ callback: async () => await page.goto(url),
99
+ });
100
+ } catch (exception) {
101
+ if (exception instanceof TimeoutException) {
102
+ // Operação "loadPage" excedeu 5000ms — retry ou aumentar limite
103
+ }
104
+ }
105
+ ```
106
+
107
+ ---
108
+
109
+ #### `InvalidArgumentException`
110
+
111
+ | | |
112
+ |---|---|
113
+ | **Trigger** | Parâmetros inválidos passados para API |
114
+ | **Quando** | `retry({ times: -1 })`, timeout negativo, argumento obrigatório ausente |
115
+ | **Extends** | `Exception` (`@odg/exception`) |
116
+ | **Handling** | Validar inputs antes de chamar API; este é um erro de programação |
117
+
118
+ ```typescript
119
+ // Este erro indica bug no código do consumidor:
120
+ try {
121
+ await retry({ times: 0, callback: async () => {} });
122
+ } catch (exception) {
123
+ if (exception instanceof InvalidArgumentException) {
124
+ // Corrigir: times deve ser >= 1
125
+ }
126
+ }
127
+ ```
128
+
129
+ ---
130
+
131
+ #### `MoneyNotFoundException`
132
+
133
+ | | |
134
+ |---|---|
135
+ | **Trigger** | `Str.money()` não encontra valor monetário na string |
136
+ | **Quando** | `new Str("texto sem valor").money()` |
137
+ | **Extends** | `Exception` (`@odg/exception`) |
138
+ | **Handling** | Verificar formato da string antes; usar try-catch ou validar conteúdo |
139
+
140
+ ```typescript
141
+ try {
142
+ const value = new Str(rawText).money();
143
+ } catch (exception) {
144
+ if (exception instanceof MoneyNotFoundException) {
145
+ // String não contém valor monetário reconhecível
146
+ }
147
+ }
148
+ ```
149
+
150
+ ---
151
+
152
+ #### `MoneyMultipleResultException`
153
+
154
+ | | |
155
+ |---|---|
156
+ | **Trigger** | `Str.money()` encontra múltiplos valores monetários na string |
157
+ | **Quando** | `new Str("R$ 10,00 e R$ 20,00").money()` — use `moneys()` |
158
+ | **Extends** | `Exception` (`@odg/exception`) |
159
+ | **Handling** | Usar `Str.moneys()` para extrair todos os valores; `money()` espera exatamente um |
160
+
161
+ ```typescript
162
+ try {
163
+ const value = new Str("R$ 10,00 e R$ 20,00").money();
164
+ } catch (exception) {
165
+ if (exception instanceof MoneyMultipleResultException) {
166
+ // Múltiplos valores encontrados — usar moneys() em vez de money()
167
+ const values = new Str("R$ 10,00 e R$ 20,00").moneys();
168
+ }
169
+ }
170
+ ```
171
+
172
+ ---
173
+
174
+ ### Handler Exception/Solution Contract
175
+
176
+ `BaseHandler.waitForHandler()` retorna `HandlerFunction`:
177
+
178
+ ```typescript
179
+ type HandlerSolutionType = Exception | Exclude<RetryAction, RetryAction.Default | RetryAction.Throw>;
180
+ type HandlerFunction = Exception | (() => Promise<HandlerSolutionType>);
181
+ ```
182
+
183
+ **Contrato:**
184
+
185
+ 1. Handler **deve** retornar `Exception` (falha) ou `() => Promise<HandlerSolutionType>` (solução)
186
+ 2. Handler **nunca** falha silenciosamente (retornar `void`/`undefined` não permitido)
187
+ 3. Solution function retorna:
188
+ - `RetryAction.Resolve` — sucesso (via `successSolution()`)
189
+ - `RetryAction.Retry` — retentar
190
+ - `Exception` — falha específica detectada na validação
191
+
192
+ **Exemplo — Handler com contrato completo:**
193
+
194
+ ```typescript
195
+ async waitForHandler(): Promise<HandlerFunction> {
196
+ // Espera seletores e decide
197
+ const errorVisible = await this.page!.$(this.$$s.error);
198
+ if (errorVisible) {
199
+ return new BrowserException("Login failed: error message visible");
200
+ }
201
+
202
+ const dashboardVisible = await this.page!.$(this.$$s.dashboard);
203
+ if (dashboardVisible) {
204
+ return async () => this.successSolution(); // RetryAction.Resolve
205
+ }
206
+
207
+ // Estado intermediário — retentar
208
+ return async () => RetryAction.Retry;
209
+ }
210
+ ```
211
+
212
+ See also: `tests/vitest/Exceptions/` e `tests/vitest/Handlers/` para exemplos de teste.