rainux-selenium-webdriver 0.0.17

Sign up to get free protection for your applications and to get access to all the features.
Files changed (192) hide show
  1. data/COPYING +204 -0
  2. data/chrome/prebuilt/Win32/Release/npchromedriver.dll +0 -0
  3. data/chrome/prebuilt/x64/Release/npchromedriver.dll +0 -0
  4. data/chrome/src/extension/background.html +9 -0
  5. data/chrome/src/extension/background.js +995 -0
  6. data/chrome/src/extension/content_script.js +1321 -0
  7. data/chrome/src/extension/icons/busy.png +0 -0
  8. data/chrome/src/extension/icons/free.png +0 -0
  9. data/chrome/src/extension/manifest-nonwin.json +19 -0
  10. data/chrome/src/extension/manifest-win.json +20 -0
  11. data/chrome/src/extension/utils.js +231 -0
  12. data/chrome/src/rb/lib/selenium/webdriver/chrome.rb +8 -0
  13. data/chrome/src/rb/lib/selenium/webdriver/chrome/bridge.rb +358 -0
  14. data/chrome/src/rb/lib/selenium/webdriver/chrome/command_executor.rb +124 -0
  15. data/chrome/src/rb/lib/selenium/webdriver/chrome/launcher.rb +135 -0
  16. data/common/src/js/abstractcommandprocessor.js +134 -0
  17. data/common/src/js/asserts.js +296 -0
  18. data/common/src/js/by.js +149 -0
  19. data/common/src/js/command.js +304 -0
  20. data/common/src/js/context.js +58 -0
  21. data/common/src/js/core/Blank.html +7 -0
  22. data/common/src/js/core/InjectedRemoteRunner.html +8 -0
  23. data/common/src/js/core/RemoteRunner.html +101 -0
  24. data/common/src/js/core/SeleniumLog.html +109 -0
  25. data/common/src/js/core/TestPrompt.html +145 -0
  26. data/common/src/js/core/TestRunner-splash.html +55 -0
  27. data/common/src/js/core/TestRunner.html +165 -0
  28. data/common/src/js/core/icons/all.png +0 -0
  29. data/common/src/js/core/icons/continue.png +0 -0
  30. data/common/src/js/core/icons/continue_disabled.png +0 -0
  31. data/common/src/js/core/icons/pause.png +0 -0
  32. data/common/src/js/core/icons/pause_disabled.png +0 -0
  33. data/common/src/js/core/icons/selected.png +0 -0
  34. data/common/src/js/core/icons/step.png +0 -0
  35. data/common/src/js/core/icons/step_disabled.png +0 -0
  36. data/common/src/js/core/lib/cssQuery/cssQuery-p.js +6 -0
  37. data/common/src/js/core/lib/cssQuery/src/cssQuery-level2.js +142 -0
  38. data/common/src/js/core/lib/cssQuery/src/cssQuery-level3.js +150 -0
  39. data/common/src/js/core/lib/cssQuery/src/cssQuery-standard.js +53 -0
  40. data/common/src/js/core/lib/cssQuery/src/cssQuery.js +356 -0
  41. data/common/src/js/core/lib/prototype.js +2006 -0
  42. data/common/src/js/core/lib/scriptaculous/builder.js +101 -0
  43. data/common/src/js/core/lib/scriptaculous/controls.js +815 -0
  44. data/common/src/js/core/lib/scriptaculous/dragdrop.js +915 -0
  45. data/common/src/js/core/lib/scriptaculous/effects.js +958 -0
  46. data/common/src/js/core/lib/scriptaculous/scriptaculous.js +47 -0
  47. data/common/src/js/core/lib/scriptaculous/slider.js +283 -0
  48. data/common/src/js/core/lib/scriptaculous/unittest.js +383 -0
  49. data/common/src/js/core/lib/snapsie.js +91 -0
  50. data/common/src/js/core/scripts/find_matching_child.js +69 -0
  51. data/common/src/js/core/scripts/htmlutils.js +8716 -0
  52. data/common/src/js/core/scripts/injection.html +72 -0
  53. data/common/src/js/core/scripts/selenium-api.js +3291 -0
  54. data/common/src/js/core/scripts/selenium-browserbot.js +2457 -0
  55. data/common/src/js/core/scripts/selenium-browserdetect.js +153 -0
  56. data/common/src/js/core/scripts/selenium-commandhandlers.js +379 -0
  57. data/common/src/js/core/scripts/selenium-executionloop.js +175 -0
  58. data/common/src/js/core/scripts/selenium-logging.js +148 -0
  59. data/common/src/js/core/scripts/selenium-remoterunner.js +695 -0
  60. data/common/src/js/core/scripts/selenium-testrunner.js +1362 -0
  61. data/common/src/js/core/scripts/selenium-version.js +5 -0
  62. data/common/src/js/core/scripts/ui-doc.html +808 -0
  63. data/common/src/js/core/scripts/ui-element.js +1644 -0
  64. data/common/src/js/core/scripts/ui-map-sample.js +979 -0
  65. data/common/src/js/core/scripts/user-extensions.js +3 -0
  66. data/common/src/js/core/scripts/user-extensions.js.sample +75 -0
  67. data/common/src/js/core/scripts/xmlextras.js +153 -0
  68. data/common/src/js/core/selenium-logo.png +0 -0
  69. data/common/src/js/core/selenium-test.css +43 -0
  70. data/common/src/js/core/selenium.css +316 -0
  71. data/common/src/js/core/xpath/dom.js +566 -0
  72. data/common/src/js/core/xpath/javascript-xpath-0.1.11.js +2816 -0
  73. data/common/src/js/core/xpath/util.js +549 -0
  74. data/common/src/js/core/xpath/xmltoken.js +149 -0
  75. data/common/src/js/core/xpath/xpath.js +2481 -0
  76. data/common/src/js/extension/README +2 -0
  77. data/common/src/js/extension/dommessenger.js +152 -0
  78. data/common/src/js/factory.js +55 -0
  79. data/common/src/js/future.js +141 -0
  80. data/common/src/js/jsunit.js +40 -0
  81. data/common/src/js/jsunit/app/css/jsUnitStyle.css +50 -0
  82. data/common/src/js/jsunit/app/css/readme +10 -0
  83. data/common/src/js/jsunit/app/emptyPage.html +11 -0
  84. data/common/src/js/jsunit/app/jsUnitCore.js +534 -0
  85. data/common/src/js/jsunit/app/jsUnitMockTimeout.js +81 -0
  86. data/common/src/js/jsunit/app/jsUnitTestManager.js +705 -0
  87. data/common/src/js/jsunit/app/jsUnitTestSuite.js +44 -0
  88. data/common/src/js/jsunit/app/jsUnitTracer.js +102 -0
  89. data/common/src/js/jsunit/app/jsUnitVersionCheck.js +59 -0
  90. data/common/src/js/jsunit/app/main-counts-errors.html +12 -0
  91. data/common/src/js/jsunit/app/main-counts-failures.html +13 -0
  92. data/common/src/js/jsunit/app/main-counts-runs.html +13 -0
  93. data/common/src/js/jsunit/app/main-counts.html +21 -0
  94. data/common/src/js/jsunit/app/main-data.html +178 -0
  95. data/common/src/js/jsunit/app/main-errors.html +23 -0
  96. data/common/src/js/jsunit/app/main-frame.html +19 -0
  97. data/common/src/js/jsunit/app/main-loader.html +45 -0
  98. data/common/src/js/jsunit/app/main-progress.html +25 -0
  99. data/common/src/js/jsunit/app/main-results.html +67 -0
  100. data/common/src/js/jsunit/app/main-status.html +13 -0
  101. data/common/src/js/jsunit/app/testContainer.html +16 -0
  102. data/common/src/js/jsunit/app/testContainerController.html +77 -0
  103. data/common/src/js/jsunit/app/xbDebug.js +306 -0
  104. data/common/src/js/jsunit/changelog.txt +60 -0
  105. data/common/src/js/jsunit/css/jsUnitStyle.css +83 -0
  106. data/common/src/js/jsunit/images/green.gif +0 -0
  107. data/common/src/js/jsunit/images/logo_jsunit.gif +0 -0
  108. data/common/src/js/jsunit/images/powerby-transparent.gif +0 -0
  109. data/common/src/js/jsunit/images/red.gif +0 -0
  110. data/common/src/js/jsunit/licenses/JDOM_license.txt +56 -0
  111. data/common/src/js/jsunit/licenses/Jetty_license.html +213 -0
  112. data/common/src/js/jsunit/licenses/MPL-1.1.txt +470 -0
  113. data/common/src/js/jsunit/licenses/gpl-2.txt +340 -0
  114. data/common/src/js/jsunit/licenses/index.html +141 -0
  115. data/common/src/js/jsunit/licenses/lgpl-2.1.txt +504 -0
  116. data/common/src/js/jsunit/licenses/mpl-tri-license-c.txt +35 -0
  117. data/common/src/js/jsunit/licenses/mpl-tri-license-html.txt +35 -0
  118. data/common/src/js/jsunit/readme.txt +19 -0
  119. data/common/src/js/jsunit/testRunner.html +167 -0
  120. data/common/src/js/jsunit/version.txt +1 -0
  121. data/common/src/js/key.js +117 -0
  122. data/common/src/js/localcommandprocessor.js +185 -0
  123. data/common/src/js/testcase.js +217 -0
  124. data/common/src/js/timing.js +89 -0
  125. data/common/src/js/webdriver.js +890 -0
  126. data/common/src/js/webelement.js +485 -0
  127. data/common/src/rb/README +30 -0
  128. data/common/src/rb/lib/selenium-webdriver.rb +1 -0
  129. data/common/src/rb/lib/selenium/webdriver.rb +67 -0
  130. data/common/src/rb/lib/selenium/webdriver/bridge_helper.rb +91 -0
  131. data/common/src/rb/lib/selenium/webdriver/child_process.rb +180 -0
  132. data/common/src/rb/lib/selenium/webdriver/core_ext/dir.rb +41 -0
  133. data/common/src/rb/lib/selenium/webdriver/driver.rb +252 -0
  134. data/common/src/rb/lib/selenium/webdriver/driver_extensions/takes_screenshot.rb +24 -0
  135. data/common/src/rb/lib/selenium/webdriver/element.rb +262 -0
  136. data/common/src/rb/lib/selenium/webdriver/error.rb +67 -0
  137. data/common/src/rb/lib/selenium/webdriver/find.rb +89 -0
  138. data/common/src/rb/lib/selenium/webdriver/keys.rb +84 -0
  139. data/common/src/rb/lib/selenium/webdriver/navigation.rb +27 -0
  140. data/common/src/rb/lib/selenium/webdriver/options.rb +50 -0
  141. data/common/src/rb/lib/selenium/webdriver/platform.rb +86 -0
  142. data/common/src/rb/lib/selenium/webdriver/target_locator.rb +70 -0
  143. data/firefox/prebuilt/Win32/Release/webdriver-firefox.dll +0 -0
  144. data/firefox/prebuilt/linux/Release/libwebdriver-firefox.so +0 -0
  145. data/firefox/prebuilt/linux/Release/x_ignore_nofocus.so +0 -0
  146. data/firefox/prebuilt/linux64/Release/libwebdriver-firefox.so +0 -0
  147. data/firefox/prebuilt/linux64/Release/x_ignore_nofocus.so +0 -0
  148. data/firefox/prebuilt/nsICommandProcessor.xpt +0 -0
  149. data/firefox/prebuilt/nsINativeEvents.xpt +0 -0
  150. data/firefox/prebuilt/nsIResponseHandler.xpt +0 -0
  151. data/firefox/src/extension/chrome.manifest +3 -0
  152. data/firefox/src/extension/components/badCertListener.js +294 -0
  153. data/firefox/src/extension/components/context.js +37 -0
  154. data/firefox/src/extension/components/driver-component.js +127 -0
  155. data/firefox/src/extension/components/firefoxDriver.js +810 -0
  156. data/firefox/src/extension/components/json2.js +273 -0
  157. data/firefox/src/extension/components/keytest.html +554 -0
  158. data/firefox/src/extension/components/nsCommandProcessor.js +643 -0
  159. data/firefox/src/extension/components/promptService.js +208 -0
  160. data/firefox/src/extension/components/screenshooter.js +81 -0
  161. data/firefox/src/extension/components/socketListener.js +185 -0
  162. data/firefox/src/extension/components/utils.js +1263 -0
  163. data/firefox/src/extension/components/webLoadingListener.js +57 -0
  164. data/firefox/src/extension/components/webdriverserver.js +106 -0
  165. data/firefox/src/extension/components/wrappedElement.js +683 -0
  166. data/firefox/src/extension/content/fxdriver.xul +30 -0
  167. data/firefox/src/extension/content/server.js +95 -0
  168. data/firefox/src/extension/idl/nsICommandProcessor.idl +38 -0
  169. data/firefox/src/extension/idl/nsIResponseHandler.idl +34 -0
  170. data/firefox/src/extension/install.rdf +29 -0
  171. data/firefox/src/rb/lib/selenium/webdriver/firefox.rb +31 -0
  172. data/firefox/src/rb/lib/selenium/webdriver/firefox/binary.rb +107 -0
  173. data/firefox/src/rb/lib/selenium/webdriver/firefox/bridge.rb +484 -0
  174. data/firefox/src/rb/lib/selenium/webdriver/firefox/extension_connection.rb +90 -0
  175. data/firefox/src/rb/lib/selenium/webdriver/firefox/launcher.rb +155 -0
  176. data/firefox/src/rb/lib/selenium/webdriver/firefox/profile.rb +233 -0
  177. data/firefox/src/rb/lib/selenium/webdriver/firefox/profiles_ini.rb +59 -0
  178. data/firefox/src/rb/lib/selenium/webdriver/firefox/util.rb +23 -0
  179. data/jobbie/prebuilt/Win32/Release/InternetExplorerDriver.dll +0 -0
  180. data/jobbie/prebuilt/x64/Release/InternetExplorerDriver.dll +0 -0
  181. data/jobbie/src/rb/lib/selenium/webdriver/ie.rb +14 -0
  182. data/jobbie/src/rb/lib/selenium/webdriver/ie/bridge.rb +565 -0
  183. data/jobbie/src/rb/lib/selenium/webdriver/ie/lib.rb +99 -0
  184. data/jobbie/src/rb/lib/selenium/webdriver/ie/util.rb +147 -0
  185. data/remote/client/src/rb/lib/selenium/webdriver/remote.rb +16 -0
  186. data/remote/client/src/rb/lib/selenium/webdriver/remote/bridge.rb +408 -0
  187. data/remote/client/src/rb/lib/selenium/webdriver/remote/capabilities.rb +105 -0
  188. data/remote/client/src/rb/lib/selenium/webdriver/remote/commands.rb +53 -0
  189. data/remote/client/src/rb/lib/selenium/webdriver/remote/default_http_client.rb +71 -0
  190. data/remote/client/src/rb/lib/selenium/webdriver/remote/response.rb +49 -0
  191. data/remote/client/src/rb/lib/selenium/webdriver/remote/server_error.rb +32 -0
  192. metadata +303 -0
@@ -0,0 +1,979 @@
1
+ // sample UI element mapping definition. This is for http://alistapart.com/,
2
+ // a particularly well structured site on web design principles.
3
+
4
+
5
+
6
+ // in general, the map should capture structural aspects of the system, instead
7
+ // of "content". In other words, interactive elements / assertible elements
8
+ // that can be counted on to always exist should be defined here. Content -
9
+ // for example text or a link that appears in a blog entry - is always liable
10
+ // to change, and will not be fun to represent in this way. You probably don't
11
+ // want to be testing specific content anyway.
12
+
13
+ // create the UI mapping object. THIS IS THE MOST IMPORTANT PART - DON'T FORGET
14
+ // TO DO THIS! In order for it to come into play, a user extension must
15
+ // construct the map in this way.
16
+ var myMap = new UIMap();
17
+
18
+
19
+
20
+
21
+ // any values which may appear multiple times can be defined as variables here.
22
+ // For example, here we're enumerating a list of top level topics that will be
23
+ // used as default argument values for several UI elements. Check out how
24
+ // this variable is referenced further down.
25
+ var topics = [
26
+ 'Code',
27
+ 'Content',
28
+ 'Culture',
29
+ 'Design',
30
+ 'Process',
31
+ 'User Science'
32
+ ];
33
+
34
+ // map subtopics to their parent topics
35
+ var subtopics = {
36
+ 'Browsers': 'Code'
37
+ , 'CSS': 'Code'
38
+ , 'Flash': 'Code'
39
+ , 'HTML and XHTML': 'Code'
40
+ , 'Scripting': 'Code'
41
+ , 'Server Side': 'Code'
42
+ , 'XML': 'Code'
43
+ , 'Brand Arts': 'Content'
44
+ , 'Community': 'Content'
45
+ , 'Writing': 'Content'
46
+ , 'Industry': 'Culture'
47
+ , 'Politics and Money': 'Culture'
48
+ , 'State of the Web': 'Culture'
49
+ , 'Graphic Design': 'Design'
50
+ , 'User Interface Design': 'Design'
51
+ , 'Typography': 'Design'
52
+ , 'Layout': 'Design'
53
+ , 'Business': 'Process'
54
+ , 'Creativity': 'Process'
55
+ , 'Project Management and Workflow': 'Process'
56
+ , 'Accessibility': 'User Science'
57
+ , 'Information Architecture': 'User Science'
58
+ , 'Usability': 'User Science'
59
+ };
60
+
61
+
62
+
63
+ // define UI elements common for all pages. This regular expression does the
64
+ // trick. '^' is automatically prepended, and '$' is automatically postpended.
65
+ // Please note that because the regular expression is being represented as a
66
+ // string, all backslashes must be escaped with an additional backslash. Also
67
+ // note that the URL being matched will always have any trailing forward slash
68
+ // stripped.
69
+ myMap.addPageset({
70
+ name: 'allPages'
71
+ , description: 'all alistapart.com pages'
72
+ , pathRegexp: '.*'
73
+ });
74
+ myMap.addElement('allPages', {
75
+ name: 'masthead'
76
+ // the description should be short and to the point, usually no longer than
77
+ // a single line
78
+ , description: 'top level image link to site homepage'
79
+ // make sure the function returns the XPath ... it's easy to leave out the
80
+ // "return" statement by accident!
81
+ , locator: "xpath=//*[@id='masthead']/a/img"
82
+ , testcase1: {
83
+ xhtml: '<h1 id="masthead"><a><img expected-result="1" /></a></h1>'
84
+ }
85
+ });
86
+ myMap.addElement('allPages', {
87
+ // be VERY CAREFUL to include commas in the correct place. Missing commas
88
+ // and extra commas can cause lots of headaches when debugging map
89
+ // definition files!!!
90
+ name: 'current_issue'
91
+ , description: 'top level link to issue currently being browsed'
92
+ , locator: "//div[@id='ish']/a"
93
+ , testcase1: {
94
+ xhtml: '<div id="ish"><a expected-result="1"></a></div>'
95
+ }
96
+ });
97
+ myMap.addElement('allPages', {
98
+ name: 'section'
99
+ , description: 'top level link to articles section'
100
+ , args: [
101
+ {
102
+ name: 'section'
103
+ , description: 'the name of the section'
104
+ , defaultValues: [
105
+ 'articles'
106
+ , 'topics'
107
+ , 'about'
108
+ , 'contact'
109
+ , 'contribute'
110
+ , 'feed'
111
+ ]
112
+ }
113
+ ]
114
+ // getXPath has been deprecated by getLocator, but verify backward
115
+ // compatability here
116
+ , getXPath: function(args) {
117
+ return "//li[@id=" + args.section.quoteForXPath() + "]/a";
118
+ }
119
+ , testcase1: {
120
+ args: { section: 'feed' }
121
+ , xhtml: '<ul><li id="feed"><a expected-result="1" /></li></ul>'
122
+ }
123
+ });
124
+ myMap.addElement('allPages', {
125
+ name: 'search_box'
126
+ , description: 'site search input field'
127
+ // xpath has been deprecated by locator, but verify backward compatability
128
+ , xpath: "//input[@id='search']"
129
+ , testcase1: {
130
+ xhtml: '<input id="search" expected-result="1" />'
131
+ }
132
+ });
133
+ myMap.addElement('allPages', {
134
+ name: 'search_discussions'
135
+ , description: 'site search include discussions checkbox'
136
+ , locator: 'incdisc'
137
+ , testcase1: {
138
+ xhtml: '<input id="incdisc" expected-result="1" />'
139
+ }
140
+ });
141
+ myMap.addElement('allPages', {
142
+ name: 'search_submit'
143
+ , description: 'site search submission button'
144
+ , locator: 'submit'
145
+ , testcase1: {
146
+ xhtml: '<input id="submit" expected-result="1" />'
147
+ }
148
+ });
149
+ myMap.addElement('allPages', {
150
+ name: 'topics'
151
+ , description: 'sidebar links to topic categories'
152
+ , args: [
153
+ {
154
+ name: 'topic'
155
+ , description: 'the name of the topic'
156
+ , defaultValues: topics
157
+ }
158
+ ]
159
+ , getLocator: function(args) {
160
+ return "//div[@id='topiclist']/ul/li" +
161
+ "/a[text()=" + args.topic.quoteForXPath() + "]";
162
+ }
163
+ , testcase1: {
164
+ args: { topic: 'foo' }
165
+ , xhtml: '<div id="topiclist"><ul><li>'
166
+ + '<a expected-result="1">foo</a>'
167
+ + '</li></ul></div>'
168
+ }
169
+ });
170
+ myMap.addElement('allPages', {
171
+ name: 'copyright'
172
+ , description: 'footer link to copyright page'
173
+ , getLocator: function(args) { return "//span[@class='copyright']/a"; }
174
+ , testcase1: {
175
+ xhtml: '<span class="copyright"><a expected-result="1" /></span>'
176
+ }
177
+ });
178
+
179
+
180
+
181
+ // define UI elements for the homepage, i.e. "http://alistapart.com/", and
182
+ // magazine issue pages, i.e. "http://alistapart.com/issues/234".
183
+ myMap.addPageset({
184
+ name: 'issuePages'
185
+ , description: 'pages including magazine issues'
186
+ , pathRegexp: '(issues/.+)?'
187
+ });
188
+ myMap.addElement('issuePages', {
189
+ name: 'article'
190
+ , description: 'front or issue page link to article'
191
+ , args: [
192
+ {
193
+ name: 'index'
194
+ , description: 'the index of the article'
195
+ // an array of default values for the argument. A default
196
+ // value is one that is passed to the getXPath() method of
197
+ // the container UIElement object when trying to build an
198
+ // element locator.
199
+ //
200
+ // range() may be used to count easily. Remember though that
201
+ // the ending value does not include the right extreme; for
202
+ // example range(1, 5) counts from 1 to 4 only.
203
+ , defaultValues: range(1, 5)
204
+ }
205
+ ]
206
+ , getLocator: function(args) {
207
+ return "//div[@class='item'][" + args.index + "]/h4/a";
208
+ }
209
+ });
210
+ myMap.addElement('issuePages', {
211
+ name: 'author'
212
+ , description: 'article author link'
213
+ , args: [
214
+ {
215
+ name: 'index'
216
+ , description: 'the index of the author, by article'
217
+ , defaultValues: range(1, 5)
218
+ }
219
+ ]
220
+ , getLocator: function(args) {
221
+ return "//div[@class='item'][" + args.index + "]/h5/a";
222
+ }
223
+ });
224
+ myMap.addElement('issuePages', {
225
+ name: 'store'
226
+ , description: 'alistapart.com store link'
227
+ , locator: "//ul[@id='banners']/li/a[@title='ALA Store']/img"
228
+ });
229
+ myMap.addElement('issuePages', {
230
+ name: 'special_article'
231
+ , description: "editor's choice article link"
232
+ , locator: "//div[@id='choice']/h4/a"
233
+ });
234
+ myMap.addElement('issuePages', {
235
+ name: 'special_author'
236
+ , description: "author link of editor's choice article"
237
+ , locator: "//div[@id='choice']/h5/a"
238
+ });
239
+
240
+
241
+
242
+ // define UI elements for the articles page, i.e.
243
+ // "http://alistapart.com/articles"
244
+ myMap.addPageset({
245
+ name: 'articleListPages'
246
+ , description: 'page with article listings'
247
+ , paths: [ 'articles' ]
248
+ });
249
+ myMap.addElement('articleListPages', {
250
+ name: 'issue'
251
+ , description: 'link to issue'
252
+ , args: [
253
+ {
254
+ name: 'index'
255
+ , description: 'the index of the issue on the page'
256
+ , defaultValues: range(1, 10)
257
+ }
258
+ ]
259
+ , getLocator: function(args) {
260
+ return "//h2[@class='ishinfo'][" + args.index + ']/a';
261
+ }
262
+ , genericLocator: "//h2[@class='ishinfo']/a"
263
+ });
264
+ myMap.addElement('articleListPages', {
265
+ name: 'article'
266
+ , description: 'link to article, by issue and article number'
267
+ , args: [
268
+ {
269
+ name: 'issue_index'
270
+ , description: "the index of the article's issue on the page; "
271
+ + 'typically five per page'
272
+ , defaultValues: range(1, 6)
273
+ }
274
+ , {
275
+ name: 'article_index'
276
+ , description: 'the index of the article within the issue; '
277
+ + 'typically two per issue'
278
+ , defaultValues: range(1, 5)
279
+ }
280
+ ]
281
+ , getLocator: function(args) {
282
+ var xpath = "//h2[@class='ishinfo'][" + (args.issue_index || 1) + ']'
283
+ + "/following-sibling::div[@class='item']"
284
+ + '[' + (args.article_index || 1) + "]/h3[@class='title']/a";
285
+ return xpath;
286
+ }
287
+ , genericLocator: "//h2[@class='ishinfo']"
288
+ + "/following-sibling::div[@class='item']/h3[@class='title']/a"
289
+ });
290
+ myMap.addElement('articleListPages', {
291
+ name: 'author'
292
+ , description: 'article author link, by issue and article'
293
+ , args: [
294
+ {
295
+ name: 'issue_index'
296
+ , description: "the index of the article's issue on the page; \
297
+ typically five per page"
298
+ , defaultValues: range(1, 6)
299
+ }
300
+ , {
301
+ name: 'article_index'
302
+ , description: "the index of the article within the issue; \
303
+ typically two articles per issue"
304
+ , defaultValues: range(1, 3)
305
+ }
306
+ ]
307
+ // this XPath uses the "following-sibling" axis. The div elements for
308
+ // the articles in an issue are not children, but siblings of the h2
309
+ // element identifying the article.
310
+ , getLocator: function(args) {
311
+ var xpath = "//h2[@class='ishinfo'][" + (args.issue_index || 1) + ']'
312
+ + "/following-sibling::div[@class='item']"
313
+ + '[' + (args.article_index || 1) + "]/h4[@class='byline']/a";
314
+ return xpath;
315
+ }
316
+ , genericLocator: "//h2[@class='ishinfo']"
317
+ + "/following-sibling::div[@class='item']/h4[@class='byline']/a"
318
+ });
319
+ myMap.addElement('articleListPages', {
320
+ name: 'next_page'
321
+ , description: 'link to next page of articles (older)'
322
+ , locator: "//a[contains(text(),'Next page')]"
323
+ });
324
+ myMap.addElement('articleListPages', {
325
+ name: 'previous_page'
326
+ , description: 'link to previous page of articles (newer)'
327
+ , locator: "//a[contains(text(),'Previous page')]"
328
+ });
329
+
330
+
331
+
332
+ // define UI elements for specific article pages, i.e.
333
+ // "http://alistapart.com/articles/culturalprobe"
334
+ myMap.addPageset({
335
+ name: 'articlePages'
336
+ , description: 'pages for actual articles'
337
+ , pathRegexp: 'articles/.+'
338
+ });
339
+ myMap.addElement('articlePages', {
340
+ name: 'title'
341
+ , description: 'article title loop-link'
342
+ , locator: "//div[@id='content']/h1[@class='title']/a"
343
+ });
344
+ myMap.addElement('articlePages', {
345
+ name: 'author'
346
+ , description: 'article author link'
347
+ , locator: "//div[@id='content']/h3[@class='byline']/a"
348
+ });
349
+ myMap.addElement('articlePages', {
350
+ name: 'article_topics'
351
+ , description: 'links to topics under which article is published, before \
352
+ article content'
353
+ , args: [
354
+ {
355
+ name: 'topic'
356
+ , description: 'the name of the topic'
357
+ , defaultValues: keys(subtopics)
358
+ }
359
+ ]
360
+ , getLocator: function(args) {
361
+ return "//ul[@id='metastuff']/li/a"
362
+ + "[@title=" + args.topic.quoteForXPath() + "]";
363
+ }
364
+ });
365
+ myMap.addElement('articlePages', {
366
+ name: 'discuss'
367
+ , description: 'link to article discussion area, before article content'
368
+ , locator: "//ul[@id='metastuff']/li[@class='discuss']/p/a"
369
+ });
370
+ myMap.addElement('articlePages', {
371
+ name: 'related_topics'
372
+ , description: 'links to topics under which article is published, after \
373
+ article content'
374
+ , args: [
375
+ {
376
+ name: 'topic'
377
+ , description: 'the name of the topic'
378
+ , defaultValues: keys(subtopics)
379
+ }
380
+ ]
381
+ , getLocator: function(args) {
382
+ return "//div[@id='learnmore']/p/a"
383
+ + "[@title=" + args.topic.quoteForXPath() + "]";
384
+ }
385
+ });
386
+ myMap.addElement('articlePages', {
387
+ name: 'join_discussion'
388
+ , description: 'link to article discussion area, after article content'
389
+ , locator: "//div[@class='discuss']/p/a"
390
+ });
391
+
392
+
393
+
394
+ myMap.addPageset({
395
+ name: 'topicListingPages'
396
+ , description: 'top level listing of topics'
397
+ , paths: [ 'topics' ]
398
+ });
399
+ myMap.addElement('topicListingPages', {
400
+ name: 'topic'
401
+ , description: 'link to topic category'
402
+ , args: [
403
+ {
404
+ name: 'topic'
405
+ , description: 'the name of the topic'
406
+ , defaultValues: topics
407
+ }
408
+ ]
409
+ , getLocator: function(args) {
410
+ return "//div[@id='content']/h2/a"
411
+ + "[text()=" + args.topic.quoteForXPath() + "]";
412
+ }
413
+ });
414
+ myMap.addElement('topicListingPages', {
415
+ name: 'subtopic'
416
+ , description: 'link to subtopic category'
417
+ , args: [
418
+ {
419
+ name: 'subtopic'
420
+ , description: 'the name of the subtopic'
421
+ , defaultValues: keys(subtopics)
422
+ }
423
+ ]
424
+ , getLocator: function(args) {
425
+ return "//div[@id='content']" +
426
+ "/descendant::a[text()=" + args.subtopic.quoteForXPath() + "]";
427
+ }
428
+ });
429
+
430
+ // the following few subtopic page UI elements are very similar. Define UI
431
+ // elements for the code page, which is a subpage under topics, i.e.
432
+ // "http://alistapart.com/topics/code/"
433
+ myMap.addPageset({
434
+ name: 'subtopicListingPages'
435
+ , description: 'pages listing subtopics'
436
+ , pathPrefix: 'topics/'
437
+ , paths: [
438
+ 'code'
439
+ , 'content'
440
+ , 'culture'
441
+ , 'design'
442
+ , 'process'
443
+ , 'userscience'
444
+ ]
445
+ });
446
+ myMap.addElement('subtopicListingPages', {
447
+ name: 'subtopic'
448
+ , description: 'link to a subtopic category'
449
+ , args: [
450
+ {
451
+ name: 'subtopic'
452
+ , description: 'the name of the subtopic'
453
+ , defaultValues: keys(subtopics)
454
+ }
455
+ ]
456
+ , getLocator: function(args) {
457
+ return "//div[@id='content']/h2" +
458
+ "/a[text()=" + args.subtopic.quoteForXPath() + "]";
459
+ }
460
+ });
461
+
462
+
463
+
464
+ // subtopic articles page
465
+ myMap.addPageset({
466
+ name: 'subtopicArticleListingPages'
467
+ , description: 'pages listing the articles for a given subtopic'
468
+ , pathRegexp: 'topics/[^/]+/.+'
469
+ });
470
+ myMap.addElement('subtopicArticleListingPages', {
471
+ name: 'article'
472
+ , description: 'link to a subtopic article'
473
+ , args: [
474
+ {
475
+ name: 'index'
476
+ , description: 'the index of the article'
477
+ , defaultValues: range(1, 51) // the range seems unlimited ...
478
+ }
479
+ ]
480
+ , getLocator: function(args) {
481
+ return "//div[@id='content']/div[@class='item']"
482
+ + "[" + args.index + "]/h3/a";
483
+ }
484
+ , testcase1: {
485
+ args: { index: 2 }
486
+ , xhtml: '<div id="content"><div class="item" /><div class="item">'
487
+ + '<h3><a expected-result="1" /></h3></div></div>'
488
+ }
489
+ });
490
+ myMap.addElement('subtopicArticleListingPages', {
491
+ name: 'author'
492
+ , description: "link to a subtopic article author's page"
493
+ , args: [
494
+ {
495
+ name: 'article_index'
496
+ , description: 'the index of the authored article'
497
+ , defaultValues: range(1, 51)
498
+ }
499
+ , {
500
+ name: 'author_index'
501
+ , description: 'the index of the author when there are multiple'
502
+ , defaultValues: range(1, 4)
503
+ }
504
+ ]
505
+ , getLocator: function(args) {
506
+ return "//div[@id='content']/div[@class='item'][" +
507
+ args.article_index + "]/h4/a[" +
508
+ (args.author_index ? args.author_index : '1') + ']';
509
+ }
510
+ });
511
+ myMap.addElement('subtopicArticleListingPages', {
512
+ name: 'issue'
513
+ , description: 'link to issue a subtopic article appears in'
514
+ , args: [
515
+ {
516
+ name: 'index'
517
+ , description: 'the index of the subtopic article'
518
+ , defaultValues: range(1, 51)
519
+ }
520
+ ]
521
+ , getLocator: function(args) {
522
+ return "//div[@id='content']/div[@class='item']"
523
+ + "[" + args.index + "]/h5/a";
524
+ }
525
+ });
526
+
527
+
528
+
529
+ myMap.addPageset({
530
+ name: 'aboutPages'
531
+ , description: 'the website about page'
532
+ , paths: [ 'about' ]
533
+ });
534
+ myMap.addElement('aboutPages', {
535
+ name: 'crew'
536
+ , description: 'link to site crew member bio or personal website'
537
+ , args: [
538
+ {
539
+ name: 'role'
540
+ , description: 'the role of the crew member'
541
+ , defaultValues: [
542
+ 'ALA Crew'
543
+ , 'Support'
544
+ , 'Emeritus'
545
+ ]
546
+ }
547
+ , {
548
+ name: 'role_index'
549
+ , description: 'the index of the member within the role'
550
+ , defaultValues: range(1, 20)
551
+ }
552
+ , {
553
+ name: 'member_index'
554
+ , description: 'the index of the member within the role title'
555
+ , defaultValues: range(1, 5)
556
+ }
557
+ ]
558
+ , getLocator: function(args) {
559
+ // the first role is kind of funky, and requires a conditional to
560
+ // build the XPath correctly. Its header looks like this:
561
+ //
562
+ // <h3>
563
+ // <span class="caps">ALA 4</span>.0 <span class="caps">CREW</span>
564
+ // </h3>
565
+ //
566
+ // This kind of complexity is a little daunting, but you can see
567
+ // how the format can handle it relatively easily and concisely.
568
+ if (args.role == 'ALA Crew') {
569
+ var selector = "descendant::text()='CREW'";
570
+ }
571
+ else {
572
+ var selector = "text()=" + args.role.quoteForXPath();
573
+ }
574
+ var xpath =
575
+ "//div[@id='secondary']/h3[" + selector + ']' +
576
+ "/following-sibling::dl/dt[" + (args.role_index || 1) + ']' +
577
+ '/a[' + (args.member_index || '1') + ']';
578
+ return xpath;
579
+ }
580
+ });
581
+
582
+
583
+
584
+ myMap.addPageset({
585
+ name: 'searchResultsPages'
586
+ , description: 'pages listing search results'
587
+ , paths: [ 'search' ]
588
+ });
589
+ myMap.addElement('searchResultsPages', {
590
+ name: 'result_link'
591
+ , description: 'search result link'
592
+ , args: [
593
+ {
594
+ name: 'index'
595
+ , description: 'the index of the search result'
596
+ , defaultValues: range(1, 11)
597
+ }
598
+ ]
599
+ , getLocator: function(args) {
600
+ return "//div[@id='content']/ul[" + args.index + ']/li/h3/a';
601
+ }
602
+ });
603
+ myMap.addElement('searchResultsPages', {
604
+ name: 'more_results_link'
605
+ , description: 'next or previous results link at top or bottom of page'
606
+ , args: [
607
+ {
608
+ name: 'direction'
609
+ , description: 'next or previous results page'
610
+ // demonstrate a method which acquires default values from the
611
+ // document object. Such default values may contain EITHER commas
612
+ // OR equals signs, but NOT BOTH.
613
+ , getDefaultValues: function(inDocument) {
614
+ var defaultValues = [];
615
+ var divs = inDocument.getElementsByTagName('div');
616
+ for (var i = 0; i < divs.length; ++i) {
617
+ if (divs[i].className == 'pages') {
618
+ break;
619
+ }
620
+ }
621
+ var links = divs[i].getElementsByTagName('a');
622
+ for (i = 0; i < links.length; ++i) {
623
+ defaultValues.push(links[i].innerHTML
624
+ .replace(/^\xab\s*/, "")
625
+ .replace(/\s*\bb$/, "")
626
+ .replace(/\s*\d+$/, ""));
627
+ }
628
+ return defaultValues;
629
+ }
630
+ }
631
+ , {
632
+ name: 'position'
633
+ , description: 'position of the link'
634
+ , defaultValues: ['top', 'bottom']
635
+ }
636
+ ]
637
+ , getLocator: function(args) {
638
+ return "//div[@id='content']/div[@class='pages']["
639
+ + (args.position == 'top' ? '1' : '2') + ']'
640
+ + "/a[contains(text(), "
641
+ + (args.direction ? args.direction.quoteForXPath() : undefined)
642
+ + ")]";
643
+ }
644
+ });
645
+
646
+
647
+
648
+ myMap.addPageset({
649
+ name: 'commentsPages'
650
+ , description: 'pages listing comments made to an article'
651
+ , pathRegexp: 'comments/.+'
652
+ });
653
+ myMap.addElement('commentsPages', {
654
+ name: 'article_link'
655
+ , description: 'link back to the original article'
656
+ , locator: "//div[@id='content']/h1[@class='title']/a"
657
+ });
658
+ myMap.addElement('commentsPages', {
659
+ name: 'comment_link'
660
+ , description: 'same-page link to comment'
661
+ , args: [
662
+ {
663
+ name: 'index'
664
+ , description: 'the index of the comment'
665
+ , defaultValues: range(1, 11)
666
+ }
667
+ ]
668
+ , getLocator: function(args) {
669
+ return "//div[@class='content']/div[contains(@class, 'comment')]" +
670
+ '[' + args.index + ']/h4/a[2]';
671
+ }
672
+ });
673
+ myMap.addElement('commentsPages', {
674
+ name: 'paging_link'
675
+ , description: 'links to more pages of comments'
676
+ , args: [
677
+ {
678
+ name: 'dest'
679
+ , description: 'the destination page'
680
+ , defaultValues: ['next', 'prev'].concat(range(1, 16))
681
+ }
682
+ , {
683
+ name: 'position'
684
+ , description: 'position of the link'
685
+ , defaultValues: ['top', 'bottom']
686
+ }
687
+ ]
688
+ , getLocator: function(args) {
689
+ var dest = args.dest;
690
+ var xpath = "//div[@id='content']/div[@class='pages']" +
691
+ '[' + (args.position == 'top' ? '1' : '2') + ']/p';
692
+ if (dest == 'next' || dest == 'prev') {
693
+ xpath += "/a[contains(text(), " + dest.quoteForXPath() + ")]";
694
+ }
695
+ else {
696
+ xpath += "/a[text()=" + dest.quoteForXPath() + "]";
697
+ }
698
+ return xpath;
699
+ }
700
+ });
701
+
702
+
703
+
704
+ myMap.addPageset({
705
+ name: 'authorPages'
706
+ , description: 'personal pages for each author'
707
+ , pathRegexp: 'authors/[a-z]/.+'
708
+ });
709
+ myMap.addElement('authorPages', {
710
+ name: 'article'
711
+ , description: "link to article written by this author.\n"
712
+ + 'This description has a line break.'
713
+ , args: [
714
+ {
715
+ name: 'index'
716
+ , description: 'index of the article on the page'
717
+ , defaultValues: range(1, 11)
718
+ }
719
+ ]
720
+ , getLocator: function(args) {
721
+ var index = args.index;
722
+ // try out the CSS locator!
723
+ //return "//h4[@class='title'][" + index + "]/a";
724
+ return 'css=h4.title:nth-child(' + index + ') > a';
725
+ }
726
+ , testcase1: {
727
+ args: { index: '2' }
728
+ , xhtml: '<h4 class="title" /><h4 class="title">'
729
+ + '<a expected-result="1" /></h4>'
730
+ }
731
+ });
732
+
733
+
734
+
735
+ // test the offset locator. Something like the following can be recorded:
736
+ // ui=qaPages::content()//a[contains(text(),'May I quote from your articles?')]
737
+ myMap.addPageset({
738
+ name: 'qaPages'
739
+ , description: 'question and answer pages'
740
+ , pathRegexp: 'qa'
741
+ });
742
+ myMap.addElement('qaPages', {
743
+ name: 'content'
744
+ , description: 'the content pane containing the q&a entries'
745
+ , locator: "//div[@id='content' and "
746
+ + "child::h1[text()='Questions and Answers']]"
747
+ , getOffsetLocator: UIElement.defaultOffsetLocatorStrategy
748
+ });
749
+ myMap.addElement('qaPages', {
750
+ name: 'last_updated'
751
+ , description: 'displays the last update date'
752
+ // demonstrate calling getLocator() for another UI element within a
753
+ // getLocator(). The former must have already been added to the map. And
754
+ // obviously, you can't randomly combine different locator types!
755
+ , locator: myMap.getUIElement('qaPages', 'content').getLocator() + '/p/em'
756
+ });
757
+
758
+
759
+
760
+ //******************************************************************************
761
+
762
+ var myRollupManager = new RollupManager();
763
+
764
+ // though the description element is required, its content is free form. You
765
+ // might want to create a documentation policy as given below, where the pre-
766
+ // and post-conditions of the rollup are spelled out.
767
+ //
768
+ // To take advantage of a "heredoc" like syntax for longer descriptions,
769
+ // add a backslash to the end of the current line and continue the string on
770
+ // the next line.
771
+ myRollupManager.addRollupRule({
772
+ name: 'navigate_to_subtopic_article_listing'
773
+ , description: 'drill down to the listing of articles for a given subtopic \
774
+ from the section menu, then the topic itself.'
775
+ , pre: 'current page contains the section menu (most pages should)'
776
+ , post: 'navigated to the page listing all articles for a given subtopic'
777
+ , args: [
778
+ {
779
+ name: 'subtopic'
780
+ , description: 'the subtopic whose article listing to navigate to'
781
+ , exampleValues: keys(subtopics)
782
+ }
783
+ ]
784
+ , commandMatchers: [
785
+ {
786
+ command: 'clickAndWait'
787
+ , target: 'ui=allPages::section\\(section=topics\\)'
788
+ // must escape parentheses in the the above target, since the
789
+ // string is being used as a regular expression. Again, backslashes
790
+ // in strings must be escaped too.
791
+ }
792
+ , {
793
+ command: 'clickAndWait'
794
+ , target: 'ui=topicListingPages::topic\\(.+'
795
+ }
796
+ , {
797
+ command: 'clickAndWait'
798
+ , target: 'ui=subtopicListingPages::subtopic\\(.+'
799
+ , updateArgs: function(command, args) {
800
+ // don't bother stripping the "ui=" prefix from the locator
801
+ // here; we're just using UISpecifier to parse the args out
802
+ var uiSpecifier = new UISpecifier(command.target);
803
+ args.subtopic = uiSpecifier.args.subtopic;
804
+ return args;
805
+ }
806
+ }
807
+ ]
808
+ , getExpandedCommands: function(args) {
809
+ var commands = [];
810
+ var topic = subtopics[args.subtopic];
811
+ var subtopic = args.subtopic;
812
+ commands.push({
813
+ command: 'clickAndWait'
814
+ , target: 'ui=allPages::section(section=topics)'
815
+ });
816
+ commands.push({
817
+ command: 'clickAndWait'
818
+ , target: 'ui=topicListingPages::topic(topic=' + topic + ')'
819
+ });
820
+ commands.push({
821
+ command: 'clickAndWait'
822
+ , target: 'ui=subtopicListingPages::subtopic(subtopic=' + subtopic
823
+ + ')'
824
+ });
825
+ commands.push({
826
+ command: 'verifyLocation'
827
+ , target: 'regexp:.+/topics/.+/.+'
828
+ });
829
+ return commands;
830
+ }
831
+ });
832
+
833
+
834
+
835
+ myRollupManager.addRollupRule({
836
+ name: 'replace_click_with_clickAndWait'
837
+ , description: 'replaces commands where a click was detected with \
838
+ clickAndWait instead'
839
+ , alternateCommand: 'clickAndWait'
840
+ , commandMatchers: [
841
+ {
842
+ command: 'click'
843
+ , target: 'ui=subtopicArticleListingPages::article\\(.+'
844
+ }
845
+ ]
846
+ , expandedCommands: []
847
+ });
848
+
849
+
850
+
851
+ myRollupManager.addRollupRule({
852
+ name: 'navigate_to_subtopic_article'
853
+ , description: 'navigate to an article listed under a subtopic.'
854
+ , pre: 'current page contains the section menu (most pages should)'
855
+ , post: 'navigated to an article page'
856
+ , args: [
857
+ {
858
+ name: 'subtopic'
859
+ , description: 'the subtopic whose article listing to navigate to'
860
+ , exampleValues: keys(subtopics)
861
+ }
862
+ , {
863
+ name: 'index'
864
+ , description: 'the index of the article in the listing'
865
+ , exampleValues: range(1, 11)
866
+ }
867
+ ]
868
+ , commandMatchers: [
869
+ {
870
+ command: 'rollup'
871
+ , target: 'navigate_to_subtopic_article_listing'
872
+ , value: 'subtopic\\s*=.+'
873
+ , updateArgs: function(command, args) {
874
+ var args1 = parse_kwargs(command.value);
875
+ args.subtopic = args1.subtopic;
876
+ return args;
877
+ }
878
+ }
879
+ , {
880
+ command: 'clickAndWait'
881
+ , target: 'ui=subtopicArticleListingPages::article\\(.+'
882
+ , updateArgs: function(command, args) {
883
+ var uiSpecifier = new UISpecifier(command.target);
884
+ args.index = uiSpecifier.args.index;
885
+ return args;
886
+ }
887
+ }
888
+ ]
889
+ /*
890
+ // this is pretty much equivalent to the commandMatchers immediately above.
891
+ // Seems more verbose and less expressive, doesn't it? But sometimes you
892
+ // might prefer the flexibility of a function.
893
+ , getRollup: function(commands) {
894
+ if (commands.length >= 2) {
895
+ command1 = commands[0];
896
+ command2 = commands[1];
897
+ var args1 = parse_kwargs(command1.value);
898
+ try {
899
+ var uiSpecifier = new UISpecifier(command2.target
900
+ .replace(/^ui=/, ''));
901
+ }
902
+ catch (e) {
903
+ return false;
904
+ }
905
+ if (command1.command == 'rollup' &&
906
+ command1.target == 'navigate_to_subtopic_article_listing' &&
907
+ args1.subtopic &&
908
+ command2.command == 'clickAndWait' &&
909
+ uiSpecifier.pagesetName == 'subtopicArticleListingPages' &&
910
+ uiSpecifier.elementName == 'article') {
911
+ var args = {
912
+ subtopic: args1.subtopic
913
+ , index: uiSpecifier.args.index
914
+ };
915
+ return {
916
+ command: 'rollup'
917
+ , target: this.name
918
+ , value: to_kwargs(args)
919
+ , replacementIndexes: [ 0, 1 ]
920
+ };
921
+ }
922
+ }
923
+ return false;
924
+ }
925
+ */
926
+ , getExpandedCommands: function(args) {
927
+ var commands = [];
928
+ commands.push({
929
+ command: 'rollup'
930
+ , target: 'navigate_to_subtopic_article_listing'
931
+ , value: to_kwargs({ subtopic: args.subtopic })
932
+ });
933
+ var uiSpecifier = new UISpecifier(
934
+ 'subtopicArticleListingPages'
935
+ , 'article'
936
+ , { index: args.index });
937
+ commands.push({
938
+ command: 'clickAndWait'
939
+ , target: 'ui=' + uiSpecifier.toString()
940
+ });
941
+ commands.push({
942
+ command: 'verifyLocation'
943
+ , target: 'regexp:.+/articles/.+'
944
+ });
945
+ return commands;
946
+ }
947
+ });
948
+
949
+
950
+
951
+
952
+
953
+
954
+
955
+
956
+
957
+
958
+
959
+
960
+
961
+
962
+
963
+
964
+
965
+
966
+
967
+
968
+
969
+
970
+
971
+
972
+
973
+
974
+
975
+
976
+
977
+
978
+
979
+