@ihazz/bitrix24 1.1.0 → 1.1.2

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.
@@ -1,94 +0,0 @@
1
- import { describe, it, expect, vi, afterEach } from 'vitest';
2
- import bitrix24Module, { collectBitrix24CallbackPaths } from '../index.ts';
3
-
4
- const runtime = {
5
- config: { loadConfig: () => ({}) },
6
- channel: {
7
- routing: { resolveAgentRoute: vi.fn() },
8
- reply: {
9
- finalizeInboundContext: vi.fn(),
10
- dispatchReplyWithBufferedBlockDispatcher: vi.fn(),
11
- },
12
- session: { recordInboundSession: vi.fn() },
13
- pairing: {
14
- readAllowFromStore: vi.fn(),
15
- upsertPairingRequest: vi.fn(),
16
- buildPairingReply: vi.fn(),
17
- },
18
- },
19
- logging: { getChildLogger: vi.fn() },
20
- };
21
-
22
- describe('collectBitrix24CallbackPaths', () => {
23
- afterEach(() => {
24
- vi.restoreAllMocks();
25
- });
26
-
27
- it('collects unique callback paths from base config and accounts', () => {
28
- const paths = collectBitrix24CallbackPaths({
29
- channels: {
30
- bitrix24: {
31
- callbackUrl: 'https://example.com/hooks/bitrix24',
32
- accounts: {
33
- a: { callbackUrl: 'https://example.com/hooks/bitrix24-a' },
34
- b: { callbackUrl: 'https://other.example.com/hooks/bitrix24-a' },
35
- },
36
- },
37
- },
38
- });
39
-
40
- expect(paths).toEqual(['/hooks/bitrix24', '/hooks/bitrix24-a']);
41
- });
42
-
43
- it('skips invalid callback urls without dropping valid ones', () => {
44
- const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
45
-
46
- const paths = collectBitrix24CallbackPaths({
47
- channels: {
48
- bitrix24: {
49
- callbackUrl: 'not-a-url',
50
- accounts: {
51
- a: { callbackUrl: 'https://example.com/hooks/bitrix24-a' },
52
- },
53
- },
54
- },
55
- });
56
-
57
- expect(paths).toEqual(['/hooks/bitrix24-a']);
58
- expect(warnSpy).toHaveBeenCalledOnce();
59
- });
60
- });
61
-
62
- describe('bitrix24 index module', () => {
63
- it('registers HTTP routes for every configured callback path', () => {
64
- const registerChannel = vi.fn();
65
- const registerHttpRoute = vi.fn();
66
-
67
- bitrix24Module.register({
68
- config: {
69
- channels: {
70
- bitrix24: {
71
- callbackUrl: 'https://example.com/hooks/bitrix24',
72
- accounts: {
73
- a: { callbackUrl: 'https://example.com/hooks/bitrix24-a' },
74
- },
75
- },
76
- },
77
- },
78
- runtime: runtime as never,
79
- registerChannel,
80
- registerHttpRoute,
81
- });
82
-
83
- expect(registerChannel).toHaveBeenCalledOnce();
84
- expect(registerHttpRoute).toHaveBeenCalledTimes(2);
85
- expect(registerHttpRoute).toHaveBeenNthCalledWith(1, expect.objectContaining({
86
- path: '/hooks/bitrix24',
87
- auth: 'plugin',
88
- }));
89
- expect(registerHttpRoute).toHaveBeenNthCalledWith(2, expect.objectContaining({
90
- path: '/hooks/bitrix24-a',
91
- auth: 'plugin',
92
- }));
93
- });
94
- });
@@ -1,319 +0,0 @@
1
- import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
- import { existsSync, unlinkSync } from 'node:fs';
3
- import { join } from 'node:path';
4
- import { MediaService, resolveManagedMediaDir } from '../src/media-service.js';
5
- import { Bitrix24Api } from '../src/api.js';
6
-
7
- const silentLogger = {
8
- info: vi.fn(),
9
- warn: vi.fn(),
10
- error: vi.fn(),
11
- debug: vi.fn(),
12
- };
13
-
14
- // Mock the API
15
- function mockApi(): Bitrix24Api {
16
- return {
17
- getFileDownloadUrl: vi.fn().mockResolvedValue('https://example.com/download/test.txt?token=abc'),
18
- uploadFile: vi.fn().mockResolvedValue({
19
- file: { id: 100, name: 'upload-test.txt' },
20
- messageId: 100,
21
- chatId: 42,
22
- dialogId: 'chat40985',
23
- }),
24
- } as unknown as Bitrix24Api;
25
- }
26
-
27
- describe('MediaService', () => {
28
- let service: MediaService;
29
- let api: Bitrix24Api;
30
- const savedPaths: string[] = [];
31
- const tempStateDir = '/tmp/openclaw-test-state';
32
-
33
- beforeEach(() => {
34
- process.env.OPENCLAW_STATE_DIR = tempStateDir;
35
- api = mockApi();
36
- service = new MediaService(api, silentLogger);
37
- vi.clearAllMocks();
38
- });
39
-
40
- afterEach(() => {
41
- // Clean up any files created during tests
42
- for (const p of savedPaths) {
43
- try { unlinkSync(p); } catch { /* ignore */ }
44
- }
45
- savedPaths.length = 0;
46
- delete process.env.OPENCLAW_STATE_DIR;
47
- delete process.env.CLAWDBOT_STATE_DIR;
48
- });
49
-
50
- describe('resolveManagedMediaDir', () => {
51
- it('uses OpenClaw state media directory by default', () => {
52
- const mediaDir = resolveManagedMediaDir({
53
- HOME: '/tmp/test-home',
54
- } as NodeJS.ProcessEnv);
55
-
56
- expect(mediaDir).toBe('/tmp/test-home/.openclaw/media/bitrix24');
57
- });
58
-
59
- it('prefers OPENCLAW_STATE_DIR when it is configured', () => {
60
- const mediaDir = resolveManagedMediaDir({
61
- OPENCLAW_STATE_DIR: '/srv/openclaw-state',
62
- HOME: '/tmp/test-home',
63
- } as NodeJS.ProcessEnv);
64
-
65
- expect(mediaDir).toBe('/srv/openclaw-state/media/bitrix24');
66
- });
67
- });
68
-
69
- describe('downloadMedia', () => {
70
- it('downloads and saves a file', async () => {
71
- const mockContent = Buffer.from('hello world');
72
- vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
73
- new Response(mockContent, { status: 200 }),
74
- );
75
-
76
- const result = await service.downloadMedia({
77
- fileId: '123',
78
- fileName: 'test.txt',
79
- extension: 'txt',
80
- webhookUrl: 'https://test.bitrix24.com/rest/1/token/',
81
- bot: { botId: 7, botToken: 'bot_token' },
82
- dialogId: 'chat40985',
83
- });
84
-
85
- expect(result).not.toBeNull();
86
- expect(result!.contentType).toBe('text/plain');
87
- expect(result!.name).toBe('test.txt');
88
- expect(result!.path).toContain('test.txt');
89
- expect(result!.path.startsWith(join(tempStateDir, 'media', 'bitrix24'))).toBe(true);
90
- expect(existsSync(result!.path)).toBe(true);
91
- savedPaths.push(result!.path);
92
-
93
- expect(api.getFileDownloadUrl).toHaveBeenCalledWith(
94
- 'https://test.bitrix24.com/rest/1/token/',
95
- { botId: 7, botToken: 'bot_token' },
96
- 'chat40985',
97
- 123,
98
- );
99
- });
100
-
101
- it('returns correct MIME for images', async () => {
102
- vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
103
- new Response(Buffer.from([0x89, 0x50, 0x4e, 0x47]), { status: 200 }),
104
- );
105
-
106
- const result = await service.downloadMedia({
107
- fileId: '456',
108
- fileName: 'photo.jpg',
109
- extension: 'jpg',
110
- webhookUrl: 'https://test.bitrix24.com/rest/1/token/',
111
- bot: { botId: 7, botToken: 'bot_token' },
112
- dialogId: 'chat40985',
113
- });
114
-
115
- expect(result).not.toBeNull();
116
- expect(result!.contentType).toBe('image/jpeg');
117
- if (result) savedPaths.push(result.path);
118
- });
119
-
120
- it('cleans up managed downloaded temp files', async () => {
121
- vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
122
- new Response(Buffer.from('cleanup me'), { status: 200 }),
123
- );
124
-
125
- const result = await service.downloadMedia({
126
- fileId: '654',
127
- fileName: 'cleanup.txt',
128
- extension: 'txt',
129
- webhookUrl: 'https://test.bitrix24.com/rest/1/token/',
130
- bot: { botId: 7, botToken: 'bot_token' },
131
- dialogId: 'chat40985',
132
- });
133
-
134
- expect(result).not.toBeNull();
135
- expect(existsSync(result!.path)).toBe(true);
136
-
137
- await service.cleanupDownloadedMedia([result!.path]);
138
-
139
- expect(existsSync(result!.path)).toBe(false);
140
- });
141
-
142
- it('rewrites private Bitrix download URLs to webhook origin', async () => {
143
- (api.getFileDownloadUrl as ReturnType<typeof vi.fn>).mockResolvedValueOnce(
144
- 'https://192.168.50.50/rest/1/token/download/?token=abc',
145
- );
146
- vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
147
- new Response(Buffer.from('hello world'), { status: 200 }),
148
- );
149
-
150
- const result = await service.downloadMedia({
151
- fileId: '777',
152
- fileName: 'private.txt',
153
- extension: 'txt',
154
- webhookUrl: 'https://test.bitrix24.com/rest/1/token/',
155
- bot: { botId: 7, botToken: 'bot_token' },
156
- dialogId: '1',
157
- });
158
-
159
- expect(result).not.toBeNull();
160
- expect(globalThis.fetch).toHaveBeenCalledWith(
161
- 'https://test.bitrix24.com/rest/1/token/download/?token=abc',
162
- expect.objectContaining({ signal: expect.any(AbortSignal) }),
163
- );
164
- if (result) savedPaths.push(result.path);
165
- });
166
-
167
- it('returns null when API fails', async () => {
168
- (api.getFileDownloadUrl as ReturnType<typeof vi.fn>).mockRejectedValueOnce(
169
- new Error('API error'),
170
- );
171
-
172
- const result = await service.downloadMedia({
173
- fileId: '789',
174
- fileName: 'fail.txt',
175
- extension: 'txt',
176
- webhookUrl: 'https://test.bitrix24.com/rest/1/token/',
177
- bot: { botId: 7, botToken: 'bot_token' },
178
- dialogId: 'chat40985',
179
- });
180
-
181
- expect(result).toBeNull();
182
- expect(silentLogger.error).toHaveBeenCalled();
183
- });
184
-
185
- it('returns null when no DOWNLOAD_URL', async () => {
186
- (api.getFileDownloadUrl as ReturnType<typeof vi.fn>).mockResolvedValueOnce('');
187
-
188
- const result = await service.downloadMedia({
189
- fileId: '789',
190
- fileName: 'nourl.txt',
191
- extension: 'txt',
192
- webhookUrl: 'https://test.bitrix24.com/rest/1/token/',
193
- bot: { botId: 7, botToken: 'bot_token' },
194
- dialogId: 'chat40985',
195
- });
196
-
197
- expect(result).toBeNull();
198
- });
199
-
200
- it('returns null when download fails', async () => {
201
- vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
202
- new Response(null, { status: 404 }),
203
- );
204
-
205
- const result = await service.downloadMedia({
206
- fileId: '789',
207
- fileName: 'missing.txt',
208
- extension: 'txt',
209
- webhookUrl: 'https://test.bitrix24.com/rest/1/token/',
210
- bot: { botId: 7, botToken: 'bot_token' },
211
- dialogId: 'chat40985',
212
- });
213
-
214
- expect(result).toBeNull();
215
- });
216
-
217
- it('handles unknown extensions with octet-stream', async () => {
218
- vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
219
- new Response(Buffer.from('data'), { status: 200 }),
220
- );
221
-
222
- const result = await service.downloadMedia({
223
- fileId: '100',
224
- fileName: 'file.xyz',
225
- extension: 'xyz',
226
- webhookUrl: 'https://test.bitrix24.com/rest/1/token/',
227
- bot: { botId: 7, botToken: 'bot_token' },
228
- dialogId: 'chat40985',
229
- });
230
-
231
- expect(result).not.toBeNull();
232
- expect(result!.contentType).toBe('application/octet-stream');
233
- if (result) savedPaths.push(result.path);
234
- });
235
-
236
- it('prefers response content type when extension is unknown', async () => {
237
- vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(
238
- new Response(Buffer.from([0xff, 0xd8, 0xff]), {
239
- status: 200,
240
- headers: { 'content-type': 'image/jpeg; charset=binary' },
241
- }),
242
- );
243
-
244
- const result = await service.downloadMedia({
245
- fileId: '101',
246
- fileName: 'forwarded-file',
247
- extension: '',
248
- webhookUrl: 'https://test.bitrix24.com/rest/1/token/',
249
- bot: { botId: 7, botToken: 'bot_token' },
250
- dialogId: 'chat40985',
251
- });
252
-
253
- expect(result).not.toBeNull();
254
- expect(result!.contentType).toBe('image/jpeg');
255
- if (result) savedPaths.push(result.path);
256
- });
257
- });
258
-
259
- describe('uploadMediaToChat', () => {
260
- it('uploads file through imbot.v2.File.upload', async () => {
261
- // Create a temp file to upload
262
- const { writeFileSync, mkdirSync } = await import('node:fs');
263
- const { join } = await import('node:path');
264
- const { tmpdir } = await import('node:os');
265
- const testDir = join(tmpdir(), 'openclaw-b24-test');
266
- mkdirSync(testDir, { recursive: true });
267
- const testPath = join(testDir, 'upload-test.txt');
268
- writeFileSync(testPath, 'upload content');
269
- savedPaths.push(testPath);
270
-
271
- const result = await service.uploadMediaToChat({
272
- localPath: testPath,
273
- fileName: 'upload-test.txt',
274
- webhookUrl: 'https://test.bitrix24.com/rest/1/token/',
275
- bot: { botId: 7, botToken: 'bot_token' },
276
- dialogId: 'chat40985',
277
- message: 'hello',
278
- });
279
-
280
- expect(result).toEqual({ ok: true, messageId: 100 });
281
- expect(api.uploadFile).toHaveBeenCalledWith(
282
- 'https://test.bitrix24.com/rest/1/token/',
283
- { botId: 7, botToken: 'bot_token' },
284
- 'chat40985',
285
- {
286
- name: 'upload-test.txt',
287
- content: Buffer.from('upload content').toString('base64'),
288
- message: 'hello',
289
- },
290
- );
291
- });
292
-
293
- it('returns { ok: false } when upload fails', async () => {
294
- (api.uploadFile as ReturnType<typeof vi.fn>).mockRejectedValueOnce(
295
- new Error('upload error'),
296
- );
297
-
298
- const { writeFileSync, mkdirSync } = await import('node:fs');
299
- const { join } = await import('node:path');
300
- const { tmpdir } = await import('node:os');
301
- const testDir = join(tmpdir(), 'openclaw-b24-test');
302
- mkdirSync(testDir, { recursive: true });
303
- const testPath = join(testDir, 'fail-upload.txt');
304
- writeFileSync(testPath, 'content');
305
- savedPaths.push(testPath);
306
-
307
- const result = await service.uploadMediaToChat({
308
- localPath: testPath,
309
- fileName: 'fail-upload.txt',
310
- webhookUrl: 'https://test.bitrix24.com/rest/1/token/',
311
- bot: { botId: 7, botToken: 'bot_token' },
312
- dialogId: 'chat40985',
313
- });
314
-
315
- expect(result).toEqual({ ok: false });
316
- expect(silentLogger.error).toHaveBeenCalled();
317
- });
318
- });
319
- });
@@ -1,184 +0,0 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { markdownToBbCode, splitMessage, buildKeyboard } from '../src/message-utils.js';
3
-
4
- describe('markdownToBbCode', () => {
5
- it('converts bold **text**', () => {
6
- expect(markdownToBbCode('This is **bold** text')).toBe('This is [B]bold[/B] text');
7
- });
8
-
9
- it('converts bold __text__', () => {
10
- expect(markdownToBbCode('This is __bold__ text')).toBe('This is [B]bold[/B] text');
11
- });
12
-
13
- it('converts italic *text*', () => {
14
- expect(markdownToBbCode('This is *italic* text')).toBe('This is [I]italic[/I] text');
15
- });
16
-
17
- it('converts italic _text_', () => {
18
- expect(markdownToBbCode('This is _italic_ text')).toBe('This is [I]italic[/I] text');
19
- });
20
-
21
- it('converts strikethrough ~~text~~', () => {
22
- expect(markdownToBbCode('This is ~~deleted~~ text')).toBe('This is [S]deleted[/S] text');
23
- });
24
-
25
- it('converts inline code', () => {
26
- expect(markdownToBbCode('Use `console.log`')).toBe('Use [CODE]console.log[/CODE]');
27
- });
28
-
29
- it('renders slash commands as bold burgundy inline accents', () => {
30
- expect(markdownToBbCode('Try `/status`')).toBe('Try [B][COLOR=#7A1F3D]/status[/COLOR][/B]');
31
- });
32
-
33
- it('renders short acronyms as bold burgundy inline accents', () => {
34
- expect(markdownToBbCode('Use `API` here')).toBe('Use [B][COLOR=#7A1F3D]API[/COLOR][/B] here');
35
- });
36
-
37
- it('converts code blocks', () => {
38
- const md = '```js\nconst x = 1;\n```';
39
- const expected = '[CODE]const x = 1;[/CODE]';
40
- expect(markdownToBbCode(md)).toBe(expected);
41
- });
42
-
43
- it('converts links [text](url)', () => {
44
- expect(markdownToBbCode('[Google](https://google.com)'))
45
- .toBe('[URL=https://google.com]Google[/URL]');
46
- });
47
-
48
- it('converts markdown images to lowercase img tag without title text', () => {
49
- expect(markdownToBbCode('![Заголовок изображения](https://via.placeholder.com/150 "Заголовок изображения")'))
50
- .toBe('[img size=medium]https://via.placeholder.com/150#image.jpg [/img]');
51
- });
52
-
53
- it('keeps supported image extensions unchanged', () => {
54
- expect(markdownToBbCode('![x](https://example.com/image.png)'))
55
- .toBe('[img size=medium]https://example.com/image.png [/img]');
56
- });
57
-
58
- it('adds image.jpg suffix for unsupported image extensions', () => {
59
- expect(markdownToBbCode('![x](https://mcgrp.ru/img/2017_05/img_1007484.avif)'))
60
- .toBe('[img size=medium]https://mcgrp.ru/img/2017_05/img_1007484.avif#image.jpg [/img]');
61
- });
62
-
63
- it('converts blockquotes', () => {
64
- expect(markdownToBbCode('> This is a quote')).toBe('>>This is a quote');
65
- });
66
-
67
- it('converts nested blockquotes to Bitrix-compatible quoted blocks', () => {
68
- expect(markdownToBbCode([
69
- '> цитата уровня 1',
70
- '>> цитата уровня два',
71
- ].join('\n'))).toBe([
72
- '>>цитата уровня 1',
73
- '>>------------------------------------------------------',
74
- '>>цитата уровня два',
75
- '>>------------------------------------------------------',
76
- ].join('\n'));
77
- });
78
-
79
- it('skips empty spacer lines inside nested blockquotes', () => {
80
- expect(markdownToBbCode([
81
- '> Это первая цитата уровня 1.',
82
- '>',
83
- '>> Это вторая цитата уровня 2, вложенная в первую.',
84
- ].join('\n'))).toBe([
85
- '>>Это первая цитата уровня 1.',
86
- '>>------------------------------------------------------',
87
- '>>Это вторая цитата уровня 2, вложенная в первую.',
88
- '>>------------------------------------------------------',
89
- ].join('\n'));
90
- });
91
-
92
- it('converts GFM tables to simplified plain text tables', () => {
93
- const input = [
94
- '| Заголовок 1 | Заголовок 2 |',
95
- '| ----------- | ----------- |',
96
- '| Ячейка 1 | Ячейка 2 |',
97
- '| Ячейка 3 | Ячейка 4 |',
98
- ].join('\n');
99
-
100
- expect(markdownToBbCode(input)).toBe([
101
- 'Заголовок 1 | Заголовок 2',
102
- '_________________________',
103
- 'Ячейка 1 | Ячейка 2',
104
- 'Ячейка 3 | Ячейка 4',
105
- ].join('\n'));
106
- });
107
-
108
- it('handles mixed formatting', () => {
109
- const input = '**Bold** and *italic* with `code`';
110
- const expected = '[B]Bold[/B] and [I]italic[/I] with [CODE]code[/CODE]';
111
- expect(markdownToBbCode(input)).toBe(expected);
112
- });
113
-
114
- it('preserves plain text', () => {
115
- expect(markdownToBbCode('Hello world')).toBe('Hello world');
116
- });
117
- });
118
-
119
- describe('splitMessage', () => {
120
- it('returns single chunk for short messages', () => {
121
- expect(splitMessage('Hello')).toEqual(['Hello']);
122
- });
123
-
124
- it('splits long messages at newlines', () => {
125
- const text = 'Line 1\nLine 2\nLine 3';
126
- const chunks = splitMessage(text, 10);
127
- expect(chunks.length).toBeGreaterThan(1);
128
- expect(chunks.join('\n')).toBe(text);
129
- });
130
-
131
- it('splits at limit when no newline found', () => {
132
- const text = 'A'.repeat(30);
133
- const chunks = splitMessage(text, 10);
134
- expect(chunks.length).toBe(3);
135
- expect(chunks.join('')).toBe(text);
136
- });
137
-
138
- it('handles exactly maxLen text', () => {
139
- const text = 'A'.repeat(100);
140
- expect(splitMessage(text, 100)).toEqual([text]);
141
- });
142
- });
143
-
144
- describe('buildKeyboard', () => {
145
- it('builds a single row keyboard (flat array)', () => {
146
- const kb = buildKeyboard([
147
- [{ text: 'Yes', command: 'answer', commandParams: 'yes' }],
148
- ]);
149
- // Flat array: 1 button, no NEWLINE
150
- expect(kb).toHaveLength(1);
151
- expect(kb[0]).toMatchObject({ TEXT: 'Yes', COMMAND: 'answer', COMMAND_PARAMS: 'yes' });
152
- });
153
-
154
- it('builds multi-row keyboard with NEWLINE separators', () => {
155
- const kb = buildKeyboard([
156
- [
157
- { text: 'Yes', command: 'answer', commandParams: 'yes' },
158
- { text: 'No', command: 'answer', commandParams: 'no' },
159
- ],
160
- [{ text: 'More info', link: 'https://example.com', fullWidth: true }],
161
- ]);
162
- // Flat: [btn, btn, NEWLINE, btn] = 4 items
163
- expect(kb).toHaveLength(4);
164
- expect(kb[0]).toMatchObject({ TEXT: 'Yes' });
165
- expect(kb[1]).toMatchObject({ TEXT: 'No' });
166
- expect(kb[2]).toMatchObject({ TYPE: 'NEWLINE' });
167
- expect(kb[3]).toMatchObject({ TEXT: 'More info', LINK: 'https://example.com', DISPLAY: 'LINE' });
168
- });
169
-
170
- it('applies default colors', () => {
171
- const kb = buildKeyboard([[{ text: 'Click' }]]);
172
- expect(kb[0]).toMatchObject({ BG_COLOR: '#29619b', TEXT_COLOR: '#fff' });
173
- });
174
-
175
- it('applies custom colors', () => {
176
- const kb = buildKeyboard([[{ text: 'Click', bgColor: '#333', textColor: '#eee' }]]);
177
- expect(kb[0]).toMatchObject({ BG_COLOR: '#333', TEXT_COLOR: '#eee' });
178
- });
179
-
180
- it('sets BLOCK=Y when disableAfterClick is true', () => {
181
- const kb = buildKeyboard([[{ text: 'Once', command: 'once', disableAfterClick: true }]]);
182
- expect(kb[0]).toMatchObject({ BLOCK: 'Y' });
183
- });
184
- });