@allwright.dev/core 0.1.2 → 0.1.4
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/dist/bootstrap.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/locator.d.ts +8 -2
- package/dist/locator.js +25 -3
- package/dist/mobile.js +32 -0
- package/dist/page.d.ts +3 -2
- package/dist/page.js +6 -3
- package/dist/selectors.d.ts +1 -1
- package/dist/selectors.js +3 -1
- package/dist/types.d.ts +11 -4
- package/dist/web-locators.d.ts +43 -0
- package/dist/web-locators.js +18 -0
- package/package.json +1 -1
- package/proto/core/v1/common.proto +2 -0
- package/proto/core/v1/context.proto +15 -1
- package/proto/core/v1/surface.proto +2 -0
- package/proto/engine/v1/engine.proto +2 -0
- package/proto/surfaces/mobile/v1/mobile.proto +2 -0
- package/proto/surfaces/web/v1/web.proto +0 -15
package/dist/bootstrap.js
CHANGED
|
@@ -12,7 +12,7 @@ const ALLWRIGHT_HOME_ENV_VAR = "ALLWRIGHT_HOME";
|
|
|
12
12
|
const ALLWRIGHT_REPOSITORY_ENV_VAR = "ALLWRIGHT_REPOSITORY";
|
|
13
13
|
const ALLWRIGHT_VERSION_ENV_VAR = "ALLWRIGHT_VERSION";
|
|
14
14
|
const DEFAULT_RELEASE_REPOSITORY = "allwright-dev/allwright";
|
|
15
|
-
const DEFAULT_RELEASE_VERSION = "0.1.
|
|
15
|
+
const DEFAULT_RELEASE_VERSION = "0.1.4";
|
|
16
16
|
const STARTUP_TIMEOUT_MS = 20_000;
|
|
17
17
|
const PING_TIMEOUT_MS = 1_000;
|
|
18
18
|
const PROTO_ROOT = fileURLToPath(new URL("../proto/", import.meta.url));
|
package/dist/index.d.ts
CHANGED
|
@@ -18,3 +18,4 @@ export declare function launchBrowser(): Promise<Browser>;
|
|
|
18
18
|
export declare function launchBrowser(options: ResolveConfigOptions): Promise<Browser>;
|
|
19
19
|
export declare function launchBrowser(browserKind: BrowserKind, options?: LaunchOptions): Promise<Browser>;
|
|
20
20
|
export { BrowserImpl, BrowserTypeImpl, PageImpl };
|
|
21
|
+
export type { TextMatcher, TextOptions, RoleOptions, LocatorFilterOptions } from "./web-locators.js";
|
package/dist/locator.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { WebLocatorBuilders, type LocatorFilterOptions } from "./web-locators.js";
|
|
1
2
|
import type { ClickResult, CommandOptions, CountResult, ElementResult, FillResult, HighlightOptions, HighlightResult, Locator, LocatorInfo, Page, PressOptions, PressResult, TextResult, WaitForSelectorOptions, WaitForSelectorResult } from "./types.js";
|
|
2
|
-
export declare class LocatorImpl implements Locator {
|
|
3
|
+
export declare class LocatorImpl extends WebLocatorBuilders implements Locator {
|
|
3
4
|
readonly page: Page;
|
|
4
5
|
readonly selector: string;
|
|
5
6
|
constructor(input: LocatorInfo);
|
|
@@ -13,5 +14,10 @@ export declare class LocatorImpl implements Locator {
|
|
|
13
14
|
textContent(options?: CommandOptions): Promise<TextResult>;
|
|
14
15
|
innerText(options?: CommandOptions): Promise<TextResult>;
|
|
15
16
|
waitFor(options?: WaitForSelectorOptions): Promise<WaitForSelectorResult>;
|
|
16
|
-
|
|
17
|
+
not(other: Locator): Locator;
|
|
18
|
+
filter(options?: LocatorFilterOptions): Locator;
|
|
19
|
+
nth(index: number): Locator;
|
|
20
|
+
first(): Locator;
|
|
21
|
+
last(): Locator;
|
|
22
|
+
locator(selector: string, options?: LocatorFilterOptions): Locator;
|
|
17
23
|
}
|
package/dist/locator.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
+
import { WebLocatorBuilders, semanticSelector } from "./web-locators.js";
|
|
1
2
|
import { chainSelectorForTransport } from "./selectors.js";
|
|
2
|
-
export class LocatorImpl {
|
|
3
|
+
export class LocatorImpl extends WebLocatorBuilders {
|
|
3
4
|
page;
|
|
4
5
|
selector;
|
|
5
6
|
constructor(input) {
|
|
7
|
+
super();
|
|
6
8
|
this.page = input.page;
|
|
7
9
|
this.selector = input.selector;
|
|
8
10
|
}
|
|
@@ -36,10 +38,30 @@ export class LocatorImpl {
|
|
|
36
38
|
async waitFor(options = {}) {
|
|
37
39
|
return this.page.waitForSelector(this.selector, options);
|
|
38
40
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
+
not(other) {
|
|
42
|
+
if (other.page !== this.page)
|
|
43
|
+
throw new Error('Excluded locators must belong to the same page');
|
|
44
|
+
return this.locator(semanticSelector({ kind: 'exclude', selector: other.selector }));
|
|
45
|
+
}
|
|
46
|
+
filter(options = {}) {
|
|
47
|
+
for (const inner of [options.has, options.hasNot]) {
|
|
48
|
+
if (inner && inner.page !== this.page)
|
|
49
|
+
throw new Error('Filter locators must belong to the same page');
|
|
50
|
+
}
|
|
51
|
+
return this.locator(semanticSelector({ ...options, kind: 'filter', has: options.has?.selector, hasNot: options.hasNot?.selector }));
|
|
52
|
+
}
|
|
53
|
+
nth(index) {
|
|
54
|
+
if (!Number.isInteger(index))
|
|
55
|
+
throw new Error('Locator index must be an integer');
|
|
56
|
+
return this.locator(semanticSelector({ kind: 'nth', index }));
|
|
57
|
+
}
|
|
58
|
+
first() { return this.nth(0); }
|
|
59
|
+
last() { return this.nth(-1); }
|
|
60
|
+
locator(selector, options) {
|
|
61
|
+
const result = new LocatorImpl({
|
|
41
62
|
page: this.page,
|
|
42
63
|
selector: chainSelectorForTransport(this.selector, selector),
|
|
43
64
|
});
|
|
65
|
+
return options ? result.filter(options) : result;
|
|
44
66
|
}
|
|
45
67
|
}
|
package/dist/mobile.js
CHANGED
|
@@ -200,6 +200,38 @@ class MobileAndroidAppImpl {
|
|
|
200
200
|
}
|
|
201
201
|
}
|
|
202
202
|
}
|
|
203
|
+
async accessibilitySnapshot(options = {}) {
|
|
204
|
+
const mode = options.mode ?? "default";
|
|
205
|
+
if (!["default", "ai", "autoexpect", "codegen"].includes(mode)) {
|
|
206
|
+
throw new Error("invalid accessibility snapshot mode");
|
|
207
|
+
}
|
|
208
|
+
const format = options.format ?? "json";
|
|
209
|
+
if (format !== "json" && format !== "yaml") {
|
|
210
|
+
throw new Error("accessibility snapshot format must be 'json' or 'yaml'");
|
|
211
|
+
}
|
|
212
|
+
const handle = await this.#getHandle();
|
|
213
|
+
this.#ensureOpen(handle);
|
|
214
|
+
handle.stream.write({
|
|
215
|
+
surfaceSessionId: this.#surfaceSessionId,
|
|
216
|
+
contextSessionId: this.sessionId,
|
|
217
|
+
accessibilitySnapshot: {
|
|
218
|
+
format,
|
|
219
|
+
mode,
|
|
220
|
+
retryOptions: options.timeoutMs ? { timeoutMs: options.timeoutMs } : undefined,
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
while (true) {
|
|
224
|
+
const event = await handle.queue.next();
|
|
225
|
+
if (event.accessibilitySnapshotCaptured)
|
|
226
|
+
return event.accessibilitySnapshotCaptured.snapshot ?? "";
|
|
227
|
+
if (event.error?.message)
|
|
228
|
+
throw new Error(event.error.message);
|
|
229
|
+
if (event.closed) {
|
|
230
|
+
handle.closed = true;
|
|
231
|
+
throw new Error(`android app session ${this.sessionId} closed while waiting for accessibility snapshot`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
203
235
|
async screenshot(options = {}) {
|
|
204
236
|
const handle = await this.#getHandle();
|
|
205
237
|
this.#ensureOpen(handle);
|
package/dist/page.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
|
+
import { WebLocatorBuilders, type LocatorFilterOptions } from "./web-locators.js";
|
|
1
2
|
import type { AccessibilitySnapshotOptions, ClickResult, CommandOptions, CountResult, ElementResult, FillResult, HighlightOptions, HighlightResult, Locator, NavigateResult, Page, PageInfo, PressOptions, PressResult, RuntimeClient, ScreenshotOptions, ScreenshotResult, TextResult, WaitForSelectorOptions, WaitForSelectorResult } from "./types.js";
|
|
2
|
-
export declare class PageImpl implements Page {
|
|
3
|
+
export declare class PageImpl extends WebLocatorBuilders implements Page {
|
|
3
4
|
#private;
|
|
4
5
|
constructor(input: PageInfo & {
|
|
5
6
|
runtime: RuntimeClient;
|
|
6
7
|
});
|
|
7
8
|
readonly sessionId: string;
|
|
8
9
|
readonly browserSessionId: string;
|
|
9
|
-
locator(selector: string): Locator;
|
|
10
|
+
locator(selector: string, options?: LocatorFilterOptions): Locator;
|
|
10
11
|
goto(url: string, options?: CommandOptions): Promise<NavigateResult>;
|
|
11
12
|
click(selector: string, options?: CommandOptions): Promise<ClickResult>;
|
|
12
13
|
count(selector: string, options?: CommandOptions): Promise<CountResult>;
|
package/dist/page.js
CHANGED
|
@@ -1,20 +1,23 @@
|
|
|
1
|
+
import { WebLocatorBuilders } from "./web-locators.js";
|
|
1
2
|
import { LocatorImpl } from "./locator.js";
|
|
2
3
|
import { writeFile } from "node:fs/promises";
|
|
3
4
|
import { formatActionError } from "./errors.js";
|
|
4
5
|
import { normalizeSelectorForTransport } from "./selectors.js";
|
|
5
6
|
import { createPageHandle } from "./runtime.js";
|
|
6
|
-
export class PageImpl {
|
|
7
|
+
export class PageImpl extends WebLocatorBuilders {
|
|
7
8
|
#runtime;
|
|
8
9
|
#handlePromise = null;
|
|
9
10
|
constructor(input) {
|
|
11
|
+
super();
|
|
10
12
|
this.#runtime = input.runtime;
|
|
11
13
|
this.sessionId = input.sessionId;
|
|
12
14
|
this.browserSessionId = input.browserSessionId;
|
|
13
15
|
}
|
|
14
16
|
sessionId;
|
|
15
17
|
browserSessionId;
|
|
16
|
-
locator(selector) {
|
|
17
|
-
|
|
18
|
+
locator(selector, options) {
|
|
19
|
+
const result = new LocatorImpl({ page: this, selector: normalizeSelectorForTransport(selector) });
|
|
20
|
+
return options ? result.filter(options) : result;
|
|
18
21
|
}
|
|
19
22
|
async goto(url, options = {}) {
|
|
20
23
|
const handle = await this.#getHandle();
|
package/dist/selectors.d.ts
CHANGED
package/dist/selectors.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const SELECTOR_PREFIXES = ["xpath=", "xpath:", "css=", "css:"];
|
|
1
|
+
const SELECTOR_PREFIXES = ["aw=", "xpath=", "xpath:", "css=", "css:"];
|
|
2
2
|
function decodeSelectorBody(body) {
|
|
3
3
|
const candidate = body.trim();
|
|
4
4
|
if (candidate.startsWith("\"") && candidate.endsWith("\"")) {
|
|
@@ -13,6 +13,8 @@ function decodeSelectorBody(body) {
|
|
|
13
13
|
}
|
|
14
14
|
function parseExplicitSelectorPrefix(selector) {
|
|
15
15
|
const lowered = selector.toLowerCase();
|
|
16
|
+
if (lowered.startsWith("aw="))
|
|
17
|
+
return { flavor: "aw", prefixLength: 3 };
|
|
16
18
|
if (lowered.startsWith("xpath=") || lowered.startsWith("xpath:")) {
|
|
17
19
|
return { flavor: "xpath", prefixLength: 6 };
|
|
18
20
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { WebLocators, LocatorFilterOptions } from "./web-locators.js";
|
|
1
2
|
import grpc from "@grpc/grpc-js";
|
|
2
3
|
export { AllwrightError } from "./errors.js";
|
|
3
4
|
export interface LaunchOptions {
|
|
@@ -108,9 +109,9 @@ export interface Browser extends BrowserInfo {
|
|
|
108
109
|
ping(message?: string): Promise<string>;
|
|
109
110
|
browserInfo(): BrowserInfo;
|
|
110
111
|
}
|
|
111
|
-
export interface Page extends PageInfo {
|
|
112
|
+
export interface Page extends PageInfo, WebLocators {
|
|
112
113
|
accessibilitySnapshot(options?: AccessibilitySnapshotOptions): Promise<string>;
|
|
113
|
-
locator(selector: string): Locator;
|
|
114
|
+
locator(selector: string, options?: LocatorFilterOptions): Locator;
|
|
114
115
|
goto(url: string, options?: CommandOptions): Promise<NavigateResult>;
|
|
115
116
|
navigate(url: string, options?: CommandOptions): Promise<NavigateResult>;
|
|
116
117
|
click(selector: string, options?: CommandOptions): Promise<ClickResult>;
|
|
@@ -155,6 +156,7 @@ export interface MobileAndroidLocator {
|
|
|
155
156
|
locator(selector: string): MobileAndroidLocator;
|
|
156
157
|
}
|
|
157
158
|
export interface MobileAndroidApp {
|
|
159
|
+
accessibilitySnapshot(options?: AccessibilitySnapshotOptions): Promise<string>;
|
|
158
160
|
readonly sessionId: string;
|
|
159
161
|
locator(selector: string): MobileAndroidLocator;
|
|
160
162
|
click(selector: string, options?: CommandOptions): Promise<ClickResult>;
|
|
@@ -178,7 +180,12 @@ export interface MobileSurfaceNamespace {
|
|
|
178
180
|
connect(options?: MobileAndroidConnectOptions): Promise<MobileAndroidDevice>;
|
|
179
181
|
};
|
|
180
182
|
}
|
|
181
|
-
export interface Locator {
|
|
183
|
+
export interface Locator extends WebLocators {
|
|
184
|
+
not(other: Locator): Locator;
|
|
185
|
+
filter(options?: LocatorFilterOptions): Locator;
|
|
186
|
+
nth(index: number): Locator;
|
|
187
|
+
first(): Locator;
|
|
188
|
+
last(): Locator;
|
|
182
189
|
readonly page: Page;
|
|
183
190
|
readonly selector: string;
|
|
184
191
|
click(options?: CommandOptions): Promise<ClickResult>;
|
|
@@ -191,7 +198,7 @@ export interface Locator {
|
|
|
191
198
|
textContent(options?: CommandOptions): Promise<TextResult>;
|
|
192
199
|
innerText(options?: CommandOptions): Promise<TextResult>;
|
|
193
200
|
waitFor(options?: WaitForSelectorOptions): Promise<WaitForSelectorResult>;
|
|
194
|
-
locator(selector: string): Locator;
|
|
201
|
+
locator(selector: string, options?: LocatorFilterOptions): Locator;
|
|
195
202
|
}
|
|
196
203
|
export interface AllwrightConfig {
|
|
197
204
|
schemaVersion?: 1;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { Locator } from './types.js';
|
|
2
|
+
export type TextMatcher = string | RegExp;
|
|
3
|
+
export interface TextOptions {
|
|
4
|
+
exact?: boolean;
|
|
5
|
+
}
|
|
6
|
+
export interface RoleOptions extends TextOptions {
|
|
7
|
+
name?: TextMatcher;
|
|
8
|
+
checked?: boolean;
|
|
9
|
+
disabled?: boolean;
|
|
10
|
+
expanded?: boolean;
|
|
11
|
+
includeHidden?: boolean;
|
|
12
|
+
level?: number;
|
|
13
|
+
pressed?: boolean;
|
|
14
|
+
selected?: boolean;
|
|
15
|
+
}
|
|
16
|
+
export interface LocatorFilterOptions {
|
|
17
|
+
has?: Locator;
|
|
18
|
+
hasNot?: Locator;
|
|
19
|
+
hasText?: TextMatcher;
|
|
20
|
+
hasNotText?: TextMatcher;
|
|
21
|
+
visible?: boolean;
|
|
22
|
+
}
|
|
23
|
+
export interface WebLocators {
|
|
24
|
+
getByRole(role: string, options?: RoleOptions): Locator;
|
|
25
|
+
getByText(text: TextMatcher, options?: TextOptions): Locator;
|
|
26
|
+
getByLabel(text: TextMatcher, options?: TextOptions): Locator;
|
|
27
|
+
getByPlaceholder(text: TextMatcher, options?: TextOptions): Locator;
|
|
28
|
+
getByAltText(text: TextMatcher, options?: TextOptions): Locator;
|
|
29
|
+
getByTitle(text: TextMatcher, options?: TextOptions): Locator;
|
|
30
|
+
getByTestId(text: TextMatcher): Locator;
|
|
31
|
+
}
|
|
32
|
+
export declare function semanticSelector(spec: Record<string, unknown>): string;
|
|
33
|
+
/** Locator construction only; DOM matching belongs to the web plugin. */
|
|
34
|
+
export declare abstract class WebLocatorBuilders implements WebLocators {
|
|
35
|
+
abstract locator(selector: string, options?: LocatorFilterOptions): Locator;
|
|
36
|
+
getByRole(role: string, options?: RoleOptions): Locator;
|
|
37
|
+
getByText(text: TextMatcher, options?: TextOptions): Locator;
|
|
38
|
+
getByLabel(text: TextMatcher, options?: TextOptions): Locator;
|
|
39
|
+
getByPlaceholder(text: TextMatcher, options?: TextOptions): Locator;
|
|
40
|
+
getByAltText(text: TextMatcher, options?: TextOptions): Locator;
|
|
41
|
+
getByTitle(text: TextMatcher, options?: TextOptions): Locator;
|
|
42
|
+
getByTestId(text: TextMatcher): Locator;
|
|
43
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export function semanticSelector(spec) {
|
|
2
|
+
return `aw=${JSON.stringify(JSON.stringify(spec, (_key, value) => value instanceof RegExp ? { regex: value.source, flags: value.flags } : value))}`;
|
|
3
|
+
}
|
|
4
|
+
/** Locator construction only; DOM matching belongs to the web plugin. */
|
|
5
|
+
export class WebLocatorBuilders {
|
|
6
|
+
getByRole(role, options = {}) {
|
|
7
|
+
if (options.level !== undefined && (!Number.isInteger(options.level) || options.level < 1)) {
|
|
8
|
+
throw new Error('Role level must be a positive integer');
|
|
9
|
+
}
|
|
10
|
+
return this.locator(semanticSelector({ ...options, kind: 'role', role }));
|
|
11
|
+
}
|
|
12
|
+
getByText(text, options = {}) { return this.locator(semanticSelector({ ...options, kind: 'text', text })); }
|
|
13
|
+
getByLabel(text, options = {}) { return this.locator(semanticSelector({ ...options, kind: 'label', text })); }
|
|
14
|
+
getByPlaceholder(text, options = {}) { return this.locator(semanticSelector({ ...options, kind: 'placeholder', text })); }
|
|
15
|
+
getByAltText(text, options = {}) { return this.locator(semanticSelector({ ...options, kind: 'altText', text })); }
|
|
16
|
+
getByTitle(text, options = {}) { return this.locator(semanticSelector({ ...options, kind: 'title', text })); }
|
|
17
|
+
getByTestId(text) { return this.locator(semanticSelector({ kind: 'testId', text })); }
|
|
18
|
+
}
|
package/package.json
CHANGED
|
@@ -6,7 +6,6 @@ option java_package = "dev.allwright.engine.v1";
|
|
|
6
6
|
option java_multiple_files = true;
|
|
7
7
|
|
|
8
8
|
import "core/v1/common.proto";
|
|
9
|
-
import "surfaces/web/v1/web.proto";
|
|
10
9
|
|
|
11
10
|
message ContextSessionCommand {
|
|
12
11
|
string surface_session_id = 1;
|
|
@@ -218,3 +217,18 @@ message ScreenshotCapturedEvent {
|
|
|
218
217
|
bytes png_data = 1;
|
|
219
218
|
string note = 2;
|
|
220
219
|
}
|
|
220
|
+
|
|
221
|
+
// Empty format defaults to JSON. Accepted values are "json" and "yaml".
|
|
222
|
+
message AccessibilitySnapshotCommand {
|
|
223
|
+
// Empty mode defaults to default. AI assigns queryable references; reference storage is surface-owned.
|
|
224
|
+
// Supported: default, ai, autoexpect, codegen.
|
|
225
|
+
string mode = 3;
|
|
226
|
+
string format = 1;
|
|
227
|
+
optional CommandRetryOptions retry_options = 2;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Both formats encode the same versioned, structured document.
|
|
231
|
+
message AccessibilitySnapshotCapturedEvent {
|
|
232
|
+
string snapshot = 1;
|
|
233
|
+
string format = 2;
|
|
234
|
+
}
|
|
@@ -2,6 +2,8 @@ syntax = "proto3";
|
|
|
2
2
|
|
|
3
3
|
package allwright.engine.v1;
|
|
4
4
|
option go_package = "allwright.dev/gen/allwright/engine/v1;enginev1";
|
|
5
|
+
option java_package = "dev.allwright.engine.v1";
|
|
6
|
+
option java_multiple_files = true;
|
|
5
7
|
|
|
6
8
|
import "core/v1/common.proto";
|
|
7
9
|
import "surfaces/mobile/v1/mobile.proto";
|
|
@@ -2,6 +2,8 @@ syntax = "proto3";
|
|
|
2
2
|
|
|
3
3
|
package allwright.engine.v1;
|
|
4
4
|
option go_package = "allwright.dev/gen/allwright/engine/v1;enginev1";
|
|
5
|
+
option java_package = "dev.allwright.engine.v1";
|
|
6
|
+
option java_multiple_files = true;
|
|
5
7
|
|
|
6
8
|
import "core/v1/surface.proto";
|
|
7
9
|
import "core/v1/common.proto";
|
|
@@ -39,18 +39,3 @@ message ChromeLaunchedEvent {
|
|
|
39
39
|
string user_data_dir = 4;
|
|
40
40
|
string initial_page_session_id = 5;
|
|
41
41
|
}
|
|
42
|
-
|
|
43
|
-
// Empty format defaults to JSON. Accepted values are "json" and "yaml".
|
|
44
|
-
message AccessibilitySnapshotCommand {
|
|
45
|
-
// Empty mode defaults to default. AI injects queryable aria-ref attributes.
|
|
46
|
-
// Supported: default, ai, autoexpect, codegen.
|
|
47
|
-
string mode = 3;
|
|
48
|
-
string format = 1;
|
|
49
|
-
optional CommandRetryOptions retry_options = 2;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Both formats encode the same versioned, structured document.
|
|
53
|
-
message AccessibilitySnapshotCapturedEvent {
|
|
54
|
-
string snapshot = 1;
|
|
55
|
-
string format = 2;
|
|
56
|
-
}
|