playwright-ruby-client 0.0.5 → 0.0.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/lib/playwright.rb +2 -0
  3. data/lib/playwright/channel_owners/element_handle.rb +75 -0
  4. data/lib/playwright/channel_owners/frame.rb +117 -0
  5. data/lib/playwright/channel_owners/page.rb +68 -11
  6. data/lib/playwright/channel_owners/request.rb +90 -0
  7. data/lib/playwright/input_type.rb +19 -0
  8. data/lib/playwright/input_types/keyboard.rb +32 -0
  9. data/lib/playwright/input_types/mouse.rb +4 -0
  10. data/lib/playwright/input_types/touchscreen.rb +4 -0
  11. data/lib/playwright/javascript/expression.rb +22 -0
  12. data/lib/playwright/javascript/function.rb +22 -0
  13. data/lib/playwright/playwright_api.rb +31 -23
  14. data/lib/playwright/timeout_settings.rb +1 -1
  15. data/lib/playwright/url_matcher.rb +19 -0
  16. data/lib/playwright/version.rb +1 -1
  17. data/lib/playwright_api/accessibility.rb +46 -6
  18. data/lib/playwright_api/binding_call.rb +6 -6
  19. data/lib/playwright_api/browser.rb +76 -16
  20. data/lib/playwright_api/browser_context.rb +284 -30
  21. data/lib/playwright_api/browser_type.rb +54 -9
  22. data/lib/playwright_api/cdp_session.rb +23 -1
  23. data/lib/playwright_api/chromium_browser_context.rb +16 -6
  24. data/lib/playwright_api/console_message.rb +10 -10
  25. data/lib/playwright_api/dialog.rb +42 -0
  26. data/lib/playwright_api/download.rb +19 -4
  27. data/lib/playwright_api/element_handle.rb +174 -21
  28. data/lib/playwright_api/file_chooser.rb +8 -0
  29. data/lib/playwright_api/frame.rb +355 -68
  30. data/lib/playwright_api/js_handle.rb +45 -9
  31. data/lib/playwright_api/keyboard.rb +98 -8
  32. data/lib/playwright_api/mouse.rb +22 -0
  33. data/lib/playwright_api/page.rb +779 -127
  34. data/lib/playwright_api/playwright.rb +98 -19
  35. data/lib/playwright_api/request.rb +70 -23
  36. data/lib/playwright_api/response.rb +6 -6
  37. data/lib/playwright_api/route.rb +48 -0
  38. data/lib/playwright_api/selectors.rb +14 -6
  39. data/lib/playwright_api/video.rb +8 -0
  40. data/lib/playwright_api/web_socket.rb +3 -5
  41. data/lib/playwright_api/worker.rb +12 -0
  42. metadata +7 -2
@@ -0,0 +1,19 @@
1
+ module Playwright
2
+ class InputType
3
+ def initialize(channel)
4
+ @channel = channel
5
+ end
6
+ end
7
+
8
+ # namespace declaration
9
+ module InputTypes ; end
10
+
11
+ def self.define_input_type(class_name, &block)
12
+ klass = Class.new(InputType)
13
+ klass.class_eval(&block) if block
14
+ InputTypes.const_set(class_name, klass)
15
+ end
16
+ end
17
+
18
+ # load subclasses
19
+ Dir[File.join(__dir__, 'input_types', '*.rb')].each { |f| require f }
@@ -0,0 +1,32 @@
1
+ module Playwright
2
+ define_input_type :Keyboard do
3
+ def down(key)
4
+ @channel.send_message_to_server('keyboardDown', key: key)
5
+ nil
6
+ end
7
+
8
+ def up(key)
9
+ @channel.send_message_to_server('keyboardDown', key: key)
10
+ end
11
+
12
+ def insert_text(text)
13
+ @channel.send_message_to_server('keyboardInsertText', text: text)
14
+ end
15
+
16
+ def type_text(text, delay: nil)
17
+ params = {
18
+ text: text,
19
+ delay: delay,
20
+ }.compact
21
+ @channel.send_message_to_server('keyboardType', params)
22
+ end
23
+
24
+ def press(key, delay: nil)
25
+ params = {
26
+ key: key,
27
+ delay: delay,
28
+ }.compact
29
+ @channel.send_message_to_server('keyboardPress', params)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,4 @@
1
+ module Playwright
2
+ define_input_type :Mouse do
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module Playwright
2
+ define_input_type :Touchscreen do
3
+ end
4
+ end
@@ -25,6 +25,28 @@ module Playwright
25
25
  )
26
26
  ::Playwright::ChannelOwner.from(resp)
27
27
  end
28
+
29
+ def eval_on_selector(channel, selector)
30
+ value = channel.send_message_to_server(
31
+ 'evalOnSelector',
32
+ selector: selector,
33
+ expression: @expression,
34
+ isFunction: false,
35
+ arg: @serialized_arg,
36
+ )
37
+ ValueParser.new(value).parse
38
+ end
39
+
40
+ def eval_on_selector_all(channel, selector)
41
+ value = channel.send_message_to_server(
42
+ 'evalOnSelectorAll',
43
+ selector: selector,
44
+ expression: @expression,
45
+ isFunction: false,
46
+ arg: @serialized_arg,
47
+ )
48
+ ValueParser.new(value).parse
49
+ end
28
50
  end
29
51
  end
30
52
  end
@@ -25,6 +25,28 @@ module Playwright
25
25
  )
26
26
  ::Playwright::ChannelOwner.from(resp)
27
27
  end
28
+
29
+ def eval_on_selector(channel, selector)
30
+ value = channel.send_message_to_server(
31
+ 'evalOnSelector',
32
+ selector: selector,
33
+ expression: @definition,
34
+ isFunction: true,
35
+ arg: @serialized_arg,
36
+ )
37
+ ValueParser.new(value).parse
38
+ end
39
+
40
+ def eval_on_selector_all(channel, selector)
41
+ value = channel.send_message_to_server(
42
+ 'evalOnSelectorAll',
43
+ selector: selector,
44
+ expression: @definition,
45
+ isFunction: true,
46
+ arg: @serialized_arg,
47
+ )
48
+ ValueParser.new(value).parse
49
+ end
28
50
  end
29
51
  end
30
52
  end
@@ -7,53 +7,58 @@ module Playwright
7
7
  # @param channel_owner [ChannelOwner]
8
8
  # @note Intended for internal use only.
9
9
  def self.from_channel_owner(channel_owner)
10
- Factory.new(channel_owner).create
10
+ Factory.new(channel_owner, 'ChannelOwners').create
11
11
  end
12
12
 
13
13
  class Factory
14
- def initialize(channel_owner)
15
- channel_owner_class_name = channel_owner.class.name
16
- raise "#{channel_owner_class_name} is not a channel owner" unless channel_owner_class_name.include?('::ChannelOwners::')
14
+ def initialize(impl, module_name)
15
+ impl_class_name = impl.class.name
16
+ unless impl_class_name.include?("::#{module_name}::")
17
+ raise "#{impl_class_name} is not #{module_name}"
18
+ end
17
19
 
18
- @channel_owner = channel_owner
20
+ @impl = impl
21
+ @module_name = module_name
19
22
  end
20
23
 
21
24
  def create
22
- api_class = detect_class_for(@channel_owner.class)
25
+ api_class = detect_class_for(@impl.class)
23
26
  if api_class
24
- api_class.new(@channel_owner)
27
+ api_class.new(@impl)
25
28
  else
26
- raise NotImplementedError.new("Playwright::#{expected_class_name_for(@channel_owner.class)} is not implemented")
29
+ raise NotImplementedError.new("Playwright::#{expected_class_name_for(@impl.class)} is not implemented")
27
30
  end
28
31
  end
29
32
 
30
33
  private
31
34
 
32
35
  def expected_class_name_for(klass)
33
- klass.name.split('::ChannelOwners::').last
36
+ klass.name.split("::#{@module_name}::").last
37
+ end
38
+
39
+ def superclass_exist?(klass)
40
+ ![::Playwright::ChannelOwner, ::Playwright::InputType, Object].include?(klass.superclass)
34
41
  end
35
42
 
36
43
  def detect_class_for(klass)
37
44
  class_name = expected_class_name_for(klass)
38
45
  if ::Playwright.const_defined?(class_name)
39
46
  ::Playwright.const_get(class_name)
47
+ elsif superclass_exist?(klass)
48
+ detect_class_for(klass.superclass)
40
49
  else
41
- if [::Playwright::ChannelOwner, Object].include?(klass.superclass)
42
- nil
43
- else
44
- detect_class_for(klass.superclass)
45
- end
50
+ nil
46
51
  end
47
52
  end
48
53
  end
49
54
 
50
- # @param channel_owner [Playwright::ChannelOwner]
51
- def initialize(channel_owner)
52
- @channel_owner = channel_owner
55
+ # @param impl [Playwright::ChannelOwner|Playwright::InputType]
56
+ def initialize(impl)
57
+ @impl = impl
53
58
  end
54
59
 
55
60
  def ==(other)
56
- @channel_owner.to_s == other.instance_variable_get(:'@channel_owner').to_s
61
+ @impl.to_s == other.instance_variable_get(:'@impl').to_s
57
62
  end
58
63
 
59
64
  # @param block [Proc]
@@ -61,16 +66,19 @@ module Playwright
61
66
  return nil unless block.is_a?(Proc)
62
67
 
63
68
  -> (*args) {
64
- wrapped_args = args.map { |arg| wrap_channel_owner(arg) }
69
+ wrapped_args = args.map { |arg| wrap_impl(arg) }
65
70
  block.call(*wrapped_args)
66
71
  }
67
72
  end
68
73
 
69
- private def wrap_channel_owner(object)
70
- if object.is_a?(ChannelOwner)
74
+ private def wrap_impl(object)
75
+ case object
76
+ when ChannelOwner
71
77
  PlaywrightApi.from_channel_owner(object)
72
- elsif object.is_a?(Array)
73
- object.map { |obj| wrap_channel_owner(obj) }
78
+ when InputType
79
+ Factory.new(object, 'InputTypes').create
80
+ when Array
81
+ object.map { |obj| wrap_impl(obj) }
74
82
  else
75
83
  object
76
84
  end
@@ -9,7 +9,7 @@ module Playwright
9
9
  attr_writer :default_timeout, :default_navigation_timeout
10
10
 
11
11
  def navigation_timeout
12
- @default_navigation_timeout || @default_timeout || @parent&.navigation_timeout || EFAULT_TIMEOUT
12
+ @default_navigation_timeout || @default_timeout || @parent&.navigation_timeout || DEFAULT_TIMEOUT
13
13
  end
14
14
 
15
15
  def timeout
@@ -0,0 +1,19 @@
1
+ module Playwright
2
+ class UrlMatcher
3
+ # @param url [String|Regexp]
4
+ def initialize(url)
5
+ @url = url
6
+ end
7
+
8
+ def match?(target_url)
9
+ case @url
10
+ when String
11
+ @url == target_url
12
+ when Regexp
13
+ @url.match?(target_url)
14
+ else
15
+ false
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Playwright
4
- VERSION = '0.0.5'
4
+ VERSION = '0.0.6'
5
5
  end
@@ -6,18 +6,18 @@ module Playwright
6
6
  # Accessibility is a very platform-specific thing. On different platforms, there are different screen readers that might
7
7
  # have wildly different output.
8
8
  #
9
- # Blink - Chromium's rendering engine - has a concept of "accessibility tree", which is then translated into different
10
- # platform-specific APIs. Accessibility namespace gives users access to the Blink Accessibility Tree.
9
+ # Rendering engines of Chromium, Firefox and Webkit have a concept of "accessibility tree", which is then translated into
10
+ # different platform-specific APIs. Accessibility namespace gives access to this Accessibility Tree.
11
11
  #
12
- # Most of the accessibility tree gets filtered out when converting from Blink AX Tree to Platform-specific AX-Tree or by
13
- # assistive technologies themselves. By default, Playwright tries to approximate this filtering, exposing only the
14
- # "interesting" nodes of the tree.
12
+ # Most of the accessibility tree gets filtered out when converting from internal browser AX Tree to Platform-specific
13
+ # AX-Tree or by assistive technologies themselves. By default, Playwright tries to approximate this filtering, exposing
14
+ # only the "interesting" nodes of the tree.
15
15
  class Accessibility < PlaywrightApi
16
16
 
17
17
  # Captures the current state of the accessibility tree. The returned object represents the root accessible node of the
18
18
  # page.
19
19
  #
20
- # > **NOTE** The Chromium accessibility tree contains nodes that go unused on most platforms and by most screen readers.
20
+ # > NOTE: The Chromium accessibility tree contains nodes that go unused on most platforms and by most screen readers.
21
21
  # Playwright will discard them as well for an easier to process tree, unless `interestingOnly` is set to `false`.
22
22
  #
23
23
  # An example of dumping the entire accessibility tree:
@@ -28,6 +28,16 @@ module Playwright
28
28
  # console.log(snapshot);
29
29
  # ```
30
30
  #
31
+ # ```python async
32
+ # snapshot = await page.accessibility.snapshot()
33
+ # print(snapshot)
34
+ # ```
35
+ #
36
+ # ```python sync
37
+ # snapshot = page.accessibility.snapshot()
38
+ # print(snapshot)
39
+ # ```
40
+ #
31
41
  # An example of logging the focused node's name:
32
42
  #
33
43
  #
@@ -46,6 +56,36 @@ module Playwright
46
56
  # return null;
47
57
  # }
48
58
  # ```
59
+ #
60
+ # ```python async
61
+ # def find_focused_node(node):
62
+ # if (node.get("focused"))
63
+ # return node
64
+ # for child in (node.get("children") or []):
65
+ # found_node = find_focused_node(child)
66
+ # return found_node
67
+ # return None
68
+ #
69
+ # snapshot = await page.accessibility.snapshot()
70
+ # node = find_focused_node(snapshot)
71
+ # if node:
72
+ # print(node["name"])
73
+ # ```
74
+ #
75
+ # ```python sync
76
+ # def find_focused_node(node):
77
+ # if (node.get("focused"))
78
+ # return node
79
+ # for child in (node.get("children") or []):
80
+ # found_node = find_focused_node(child)
81
+ # return found_node
82
+ # return None
83
+ #
84
+ # snapshot = page.accessibility.snapshot()
85
+ # node = find_focused_node(snapshot)
86
+ # if node:
87
+ # print(node["name"])
88
+ # ```
49
89
  def snapshot(interestingOnly: nil, root: nil)
50
90
  raise NotImplementedError.new('snapshot is not implemented yet.')
51
91
  end
@@ -4,20 +4,20 @@ module Playwright
4
4
 
5
5
  # -- inherited from EventEmitter --
6
6
  # @nodoc
7
- def off(event, callback)
8
- wrap_channel_owner(@channel_owner.off(event, callback))
7
+ def on(event, callback)
8
+ wrap_impl(@impl.on(event, callback))
9
9
  end
10
10
 
11
11
  # -- inherited from EventEmitter --
12
12
  # @nodoc
13
- def once(event, callback)
14
- wrap_channel_owner(@channel_owner.once(event, callback))
13
+ def off(event, callback)
14
+ wrap_impl(@impl.off(event, callback))
15
15
  end
16
16
 
17
17
  # -- inherited from EventEmitter --
18
18
  # @nodoc
19
- def on(event, callback)
20
- wrap_channel_owner(@channel_owner.on(event, callback))
19
+ def once(event, callback)
20
+ wrap_impl(@impl.once(event, callback))
21
21
  end
22
22
  end
23
23
  end
@@ -1,5 +1,5 @@
1
1
  module Playwright
2
- # - extends: [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter)
2
+ # - extends: [EventEmitter]
3
3
  #
4
4
  # A Browser is created via [`method: BrowserType.launch`]. An example of using a `Browser` to create a [Page]:
5
5
  #
@@ -15,8 +15,36 @@ module Playwright
15
15
  # })();
16
16
  # ```
17
17
  #
18
- # See `ChromiumBrowser`, [FirefoxBrowser] and [WebKitBrowser] for browser-specific features. Note that
19
- # [`method: BrowserType.launch`] always returns a specific browser instance, based on the browser being launched.
18
+ # ```python async
19
+ # import asyncio
20
+ # from playwright.async_api import async_playwright
21
+ #
22
+ # async def run(playwright):
23
+ # firefox = playwright.firefox
24
+ # browser = await firefox.launch()
25
+ # page = await browser.new_page()
26
+ # await page.goto("https://example.com")
27
+ # await browser.close()
28
+ #
29
+ # async def main():
30
+ # async with async_playwright() as playwright:
31
+ # await run(playwright)
32
+ # asyncio.run(main())
33
+ # ```
34
+ #
35
+ # ```python sync
36
+ # from playwright.sync_api import sync_playwright
37
+ #
38
+ # def run(playwright):
39
+ # firefox = playwright.firefox
40
+ # browser = firefox.launch()
41
+ # page = browser.new_page()
42
+ # page.goto("https://example.com")
43
+ # browser.close()
44
+ #
45
+ # with sync_playwright() as playwright:
46
+ # run(playwright)
47
+ # ```
20
48
  class Browser < PlaywrightApi
21
49
 
22
50
  # In case this browser is obtained using [`method: BrowserType.launch`], closes the browser and all of its pages (if any
@@ -27,7 +55,7 @@ module Playwright
27
55
  #
28
56
  # The `Browser` object itself is considered to be disposed and cannot be used anymore.
29
57
  def close
30
- wrap_channel_owner(@channel_owner.close)
58
+ wrap_impl(@impl.close)
31
59
  end
32
60
 
33
61
  # Returns an array of all open browser contexts. In a newly created browser, this will return zero browser contexts.
@@ -40,13 +68,27 @@ module Playwright
40
68
  # const context = await browser.newContext();
41
69
  # console.log(browser.contexts().length); // prints `1`
42
70
  # ```
71
+ #
72
+ # ```python async
73
+ # browser = await pw.webkit.launch()
74
+ # print(len(browser.contexts())) # prints `0`
75
+ # context = await browser.new_context()
76
+ # print(len(browser.contexts())) # prints `1`
77
+ # ```
78
+ #
79
+ # ```python sync
80
+ # browser = pw.webkit.launch()
81
+ # print(len(browser.contexts())) # prints `0`
82
+ # context = browser.new_context()
83
+ # print(len(browser.contexts())) # prints `1`
84
+ # ```
43
85
  def contexts
44
- wrap_channel_owner(@channel_owner.contexts)
86
+ wrap_impl(@impl.contexts)
45
87
  end
46
88
 
47
89
  # Indicates that the browser is connected.
48
90
  def connected?
49
- wrap_channel_owner(@channel_owner.connected?)
91
+ wrap_impl(@impl.connected?)
50
92
  end
51
93
 
52
94
  # Creates a new browser context. It won't share cookies/cache with other browser contexts.
@@ -62,6 +104,24 @@ module Playwright
62
104
  # await page.goto('https://example.com');
63
105
  # })();
64
106
  # ```
107
+ #
108
+ # ```python async
109
+ # browser = await playwright.firefox.launch() # or "chromium" or "webkit".
110
+ # # create a new incognito browser context.
111
+ # context = await browser.new_context()
112
+ # # create a new page in a pristine context.
113
+ # page = await context.new_page()
114
+ # await page.goto("https://example.com")
115
+ # ```
116
+ #
117
+ # ```python sync
118
+ # browser = playwright.firefox.launch() # or "chromium" or "webkit".
119
+ # # create a new incognito browser context.
120
+ # context = browser.new_context()
121
+ # # create a new page in a pristine context.
122
+ # page = context.new_page()
123
+ # page.goto("https://example.com")
124
+ # ```
65
125
  def new_context(
66
126
  acceptDownloads: nil,
67
127
  bypassCSP: nil,
@@ -87,7 +147,7 @@ module Playwright
87
147
  videoSize: nil,
88
148
  videosPath: nil,
89
149
  viewport: nil)
90
- wrap_channel_owner(@channel_owner.new_context(acceptDownloads: acceptDownloads, bypassCSP: bypassCSP, colorScheme: colorScheme, deviceScaleFactor: deviceScaleFactor, extraHTTPHeaders: extraHTTPHeaders, geolocation: geolocation, hasTouch: hasTouch, httpCredentials: httpCredentials, ignoreHTTPSErrors: ignoreHTTPSErrors, isMobile: isMobile, javaScriptEnabled: javaScriptEnabled, locale: locale, logger: logger, offline: offline, permissions: permissions, proxy: proxy, recordHar: recordHar, recordVideo: recordVideo, storageState: storageState, timezoneId: timezoneId, userAgent: userAgent, videoSize: videoSize, videosPath: videosPath, viewport: viewport))
150
+ wrap_impl(@impl.new_context(acceptDownloads: acceptDownloads, bypassCSP: bypassCSP, colorScheme: colorScheme, deviceScaleFactor: deviceScaleFactor, extraHTTPHeaders: extraHTTPHeaders, geolocation: geolocation, hasTouch: hasTouch, httpCredentials: httpCredentials, ignoreHTTPSErrors: ignoreHTTPSErrors, isMobile: isMobile, javaScriptEnabled: javaScriptEnabled, locale: locale, logger: logger, offline: offline, permissions: permissions, proxy: proxy, recordHar: recordHar, recordVideo: recordVideo, storageState: storageState, timezoneId: timezoneId, userAgent: userAgent, videoSize: videoSize, videosPath: videosPath, viewport: viewport))
91
151
  end
92
152
 
93
153
  # Creates a new page in a new browser context. Closing this page will close the context as well.
@@ -120,35 +180,35 @@ module Playwright
120
180
  videoSize: nil,
121
181
  videosPath: nil,
122
182
  viewport: nil)
123
- wrap_channel_owner(@channel_owner.new_page(acceptDownloads: acceptDownloads, bypassCSP: bypassCSP, colorScheme: colorScheme, deviceScaleFactor: deviceScaleFactor, extraHTTPHeaders: extraHTTPHeaders, geolocation: geolocation, hasTouch: hasTouch, httpCredentials: httpCredentials, ignoreHTTPSErrors: ignoreHTTPSErrors, isMobile: isMobile, javaScriptEnabled: javaScriptEnabled, locale: locale, logger: logger, offline: offline, permissions: permissions, proxy: proxy, recordHar: recordHar, recordVideo: recordVideo, storageState: storageState, timezoneId: timezoneId, userAgent: userAgent, videoSize: videoSize, videosPath: videosPath, viewport: viewport))
183
+ wrap_impl(@impl.new_page(acceptDownloads: acceptDownloads, bypassCSP: bypassCSP, colorScheme: colorScheme, deviceScaleFactor: deviceScaleFactor, extraHTTPHeaders: extraHTTPHeaders, geolocation: geolocation, hasTouch: hasTouch, httpCredentials: httpCredentials, ignoreHTTPSErrors: ignoreHTTPSErrors, isMobile: isMobile, javaScriptEnabled: javaScriptEnabled, locale: locale, logger: logger, offline: offline, permissions: permissions, proxy: proxy, recordHar: recordHar, recordVideo: recordVideo, storageState: storageState, timezoneId: timezoneId, userAgent: userAgent, videoSize: videoSize, videosPath: videosPath, viewport: viewport))
124
184
  end
125
185
 
126
186
  # Returns the browser version.
127
187
  def version
128
- wrap_channel_owner(@channel_owner.version)
188
+ wrap_impl(@impl.version)
129
189
  end
130
190
 
131
191
  # @nodoc
132
192
  def after_initialize
133
- wrap_channel_owner(@channel_owner.after_initialize)
193
+ wrap_impl(@impl.after_initialize)
134
194
  end
135
195
 
136
196
  # -- inherited from EventEmitter --
137
197
  # @nodoc
138
- def off(event, callback)
139
- wrap_channel_owner(@channel_owner.off(event, callback))
198
+ def on(event, callback)
199
+ wrap_impl(@impl.on(event, callback))
140
200
  end
141
201
 
142
202
  # -- inherited from EventEmitter --
143
203
  # @nodoc
144
- def once(event, callback)
145
- wrap_channel_owner(@channel_owner.once(event, callback))
204
+ def off(event, callback)
205
+ wrap_impl(@impl.off(event, callback))
146
206
  end
147
207
 
148
208
  # -- inherited from EventEmitter --
149
209
  # @nodoc
150
- def on(event, callback)
151
- wrap_channel_owner(@channel_owner.on(event, callback))
210
+ def once(event, callback)
211
+ wrap_impl(@impl.once(event, callback))
152
212
  end
153
213
  end
154
214
  end