capybara-webkit 0.14.2 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. data/.gitignore +2 -0
  2. data/.travis.yml +21 -0
  3. data/Appraisals +4 -4
  4. data/CONTRIBUTING.md +14 -3
  5. data/Gemfile +1 -1
  6. data/Gemfile.lock +27 -19
  7. data/NEWS.md +15 -0
  8. data/README.md +126 -76
  9. data/Vagrantfile +7 -0
  10. data/capybara-webkit.gemspec +3 -0
  11. data/gemfiles/2.0.gemfile +7 -0
  12. data/gemfiles/2.0.gemfile.lock +72 -0
  13. data/gemfiles/2.1.gemfile +7 -0
  14. data/gemfiles/2.1.gemfile.lock +71 -0
  15. data/lib/capybara/webkit/browser.rb +22 -22
  16. data/lib/capybara/webkit/connection.rb +9 -6
  17. data/lib/capybara/webkit/driver.rb +22 -6
  18. data/lib/capybara/webkit/errors.rb +25 -0
  19. data/lib/capybara/webkit/node.rb +36 -10
  20. data/lib/capybara/webkit/version.rb +1 -1
  21. data/spec/browser_spec.rb +16 -1
  22. data/spec/capybara_webkit_builder_spec.rb +9 -3
  23. data/spec/connection_spec.rb +19 -3
  24. data/spec/driver_spec.rb +324 -144
  25. data/spec/errors_spec.rb +11 -0
  26. data/spec/integration/session_spec.rb +244 -0
  27. data/spec/selenium_compatibility_spec.rb +3 -1
  28. data/spec/spec_helper.rb +1 -9
  29. data/src/Authenticate.cpp +3 -2
  30. data/src/ClearCookies.cpp +1 -1
  31. data/src/ClearPromptText.cpp +1 -1
  32. data/src/Command.cpp +8 -4
  33. data/src/Command.h +7 -4
  34. data/src/CommandFactory.cpp +4 -2
  35. data/src/CommandParser.cpp +1 -1
  36. data/src/Connection.cpp +4 -4
  37. data/src/ConsoleMessages.cpp +1 -1
  38. data/src/CurrentUrl.cpp +2 -2
  39. data/src/EnableLogging.cpp +1 -1
  40. data/src/ErrorMessage.cpp +26 -0
  41. data/src/ErrorMessage.h +21 -0
  42. data/src/Evaluate.cpp +1 -1
  43. data/src/Execute.cpp +3 -2
  44. data/src/FindCss.cpp +13 -0
  45. data/src/FindCss.h +11 -0
  46. data/src/FindXpath.cpp +13 -0
  47. data/src/FindXpath.h +11 -0
  48. data/src/FrameFocus.cpp +4 -3
  49. data/src/GetCookies.cpp +1 -1
  50. data/src/GetTimeout.cpp +1 -1
  51. data/src/GetWindowHandle.cpp +1 -1
  52. data/src/GetWindowHandles.cpp +1 -1
  53. data/src/Header.cpp +2 -2
  54. data/src/Headers.cpp +1 -6
  55. data/src/IgnoreSslErrors.cpp +1 -1
  56. data/src/InvocationResult.cpp +29 -0
  57. data/src/InvocationResult.h +16 -0
  58. data/src/JavascriptAlertMessages.cpp +1 -1
  59. data/src/JavascriptCommand.cpp +15 -0
  60. data/src/JavascriptCommand.h +20 -0
  61. data/src/JavascriptConfirmMessages.cpp +1 -1
  62. data/src/JavascriptInvocation.cpp +128 -1
  63. data/src/JavascriptInvocation.h +22 -1
  64. data/src/JavascriptPromptMessages.cpp +1 -1
  65. data/src/NetworkAccessManager.cpp +8 -16
  66. data/src/NetworkAccessManager.h +5 -11
  67. data/src/NetworkReplyProxy.cpp +91 -0
  68. data/src/NetworkReplyProxy.h +65 -0
  69. data/src/Node.cpp +4 -4
  70. data/src/Node.h +2 -2
  71. data/src/NullCommand.cpp +2 -1
  72. data/src/PageLoadingCommand.cpp +2 -1
  73. data/src/Render.cpp +1 -1
  74. data/src/Reset.cpp +1 -1
  75. data/src/ResizeWindow.cpp +1 -1
  76. data/src/Response.cpp +7 -0
  77. data/src/Response.h +8 -3
  78. data/src/SetConfirmAction.cpp +1 -1
  79. data/src/SetCookie.cpp +2 -2
  80. data/src/SetPromptAction.cpp +1 -1
  81. data/src/SetPromptText.cpp +1 -1
  82. data/src/SetProxy.cpp +2 -2
  83. data/src/SetSkipImageLoading.cpp +1 -1
  84. data/src/SetTimeout.cpp +3 -2
  85. data/src/SetUrlBlacklist.cpp +2 -2
  86. data/src/Status.cpp +1 -1
  87. data/src/TimeoutCommand.cpp +4 -2
  88. data/src/Title.cpp +11 -0
  89. data/src/Title.h +9 -0
  90. data/src/Version.cpp +13 -0
  91. data/src/Version.h +10 -0
  92. data/src/Visit.cpp +1 -1
  93. data/src/WebPage.cpp +49 -27
  94. data/src/WebPage.h +14 -7
  95. data/src/WebPageManager.cpp +10 -1
  96. data/src/WebPageManager.h +4 -1
  97. data/src/WindowFocus.cpp +3 -2
  98. data/src/body.cpp +3 -6
  99. data/src/capybara.js +103 -101
  100. data/src/find_command.h +4 -2
  101. data/src/main.cpp +1 -1
  102. data/src/stable.h +39 -0
  103. data/src/webkit_server.pro +26 -6
  104. data/vagrant_setup.sh +58 -0
  105. metadata +51 -78
  106. data/gemfiles/1.0.gemfile +0 -7
  107. data/gemfiles/1.0.gemfile.lock +0 -70
  108. data/gemfiles/1.1.gemfile +0 -7
  109. data/gemfiles/1.1.gemfile.lock +0 -70
  110. data/src/Find.cpp +0 -20
  111. data/src/Find.h +0 -11
data/src/WindowFocus.cpp CHANGED
@@ -3,6 +3,7 @@
3
3
  #include "WebPage.h"
4
4
  #include "CommandFactory.h"
5
5
  #include "WebPageManager.h"
6
+ #include "ErrorMessage.h"
6
7
 
7
8
  WindowFocus::WindowFocus(WebPageManager *manager, QStringList &arguments, QObject *parent) : SocketCommand(manager, arguments, parent) {
8
9
  }
@@ -12,12 +13,12 @@ void WindowFocus::start() {
12
13
  }
13
14
 
14
15
  void WindowFocus::windowNotFound() {
15
- emitFinished(false, QString("Unable to locate window. "));
16
+ finish(false, new ErrorMessage("Unable to locate window."));
16
17
  }
17
18
 
18
19
  void WindowFocus::success(WebPage *page) {
19
20
  page->setFocus();
20
- emitFinished(true);
21
+ finish(true);
21
22
  }
22
23
 
23
24
  void WindowFocus::focusWindow(QString selector) {
data/src/body.cpp CHANGED
@@ -6,11 +6,8 @@ Body::Body(WebPageManager *manager, QStringList &arguments, QObject *parent) : S
6
6
  }
7
7
 
8
8
  void Body::start() {
9
- QString result;
10
- if (page()->unsupportedContentLoaded())
11
- result = page()->currentFrame()->toPlainText();
9
+ if (page()->contentType().contains("html"))
10
+ finish(true, page()->currentFrame()->toHtml());
12
11
  else
13
- result = page()->currentFrame()->toHtml();
14
-
15
- emitFinished(true, result);
12
+ finish(true, page()->body());
16
13
  }
data/src/capybara.js CHANGED
@@ -4,22 +4,30 @@ Capybara = {
4
4
  attachedFiles: [],
5
5
 
6
6
  invoke: function () {
7
- return this[CapybaraInvocation.functionName].apply(this, CapybaraInvocation.arguments);
7
+ try {
8
+ return this[CapybaraInvocation.functionName].apply(this, CapybaraInvocation.arguments);
9
+ } catch (e) {
10
+ CapybaraInvocation.error = e;
11
+ }
12
+ },
13
+
14
+ findXpath: function (xpath) {
15
+ return this.findXpathRelativeTo(document, xpath);
8
16
  },
9
17
 
10
- find: function (xpath) {
11
- return this.findRelativeTo(document, xpath);
18
+ findCss: function (selector) {
19
+ return this.findCssRelativeTo(document, selector);
12
20
  },
13
21
 
14
- currentUrl: function () {
15
- return window.location.toString();
22
+ findXpathWithin: function (index, xpath) {
23
+ return this.findXpathRelativeTo(this.nodes[index], xpath);
16
24
  },
17
25
 
18
- findWithin: function (index, xpath) {
19
- return this.findRelativeTo(this.nodes[index], xpath);
26
+ findCssWithin: function (index, selector) {
27
+ return this.findCssRelativeTo(this.nodes[index], selector);
20
28
  },
21
29
 
22
- findRelativeTo: function (reference, xpath) {
30
+ findXpathRelativeTo: function (reference, xpath) {
23
31
  var iterator = document.evaluate(xpath, reference, null, XPathResult.ORDERED_NODE_ITERATOR_TYPE, null);
24
32
  var node;
25
33
  var results = [];
@@ -31,8 +39,20 @@ Capybara = {
31
39
  return results.join(",");
32
40
  },
33
41
 
42
+ findCssRelativeTo: function (reference, selector) {
43
+ var elements = reference.querySelectorAll(selector);
44
+ var results = [];
45
+ for (var i = 0; i < elements.length; i++) {
46
+ this.nextIndex++;
47
+ this.nodes[this.nextIndex] = elements[i];
48
+ results.push(this.nextIndex);
49
+ }
50
+ return results.join(",");
51
+ },
52
+
34
53
  isAttached: function(index) {
35
- return document.evaluate("ancestor-or-self::html", this.nodes[index], null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue != null;
54
+ return this.nodes[index] &&
55
+ document.evaluate("ancestor-or-self::html", this.nodes[index], null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue != null;
36
56
  },
37
57
 
38
58
  text: function (index) {
@@ -41,10 +61,15 @@ Capybara = {
41
61
  if (type == "textarea") {
42
62
  return node.innerHTML;
43
63
  } else {
44
- return node.innerText;
64
+ return node.innerText || node.textContent;
45
65
  }
46
66
  },
47
67
 
68
+ allText: function (index) {
69
+ var node = this.nodes[index];
70
+ return node.textContent;
71
+ },
72
+
48
73
  attribute: function (index, name) {
49
74
  switch(name) {
50
75
  case 'checked':
@@ -108,53 +133,67 @@ Capybara = {
108
133
  return this.nodes[index].submit();
109
134
  },
110
135
 
111
- mousedown: function(index) {
112
- var mousedownEvent = document.createEvent('MouseEvents');
113
- mousedownEvent.initMouseEvent('mousedown', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
114
- this.nodes[index].dispatchEvent(mousedownEvent);
136
+ clickTest: function(node, pos) {
137
+ var el = document.elementFromPoint(pos.relativeX, pos.relativeY);
138
+
139
+ while (el) {
140
+ if (el === node)
141
+ return CapybaraInvocation.clickTest(node, pos.absoluteX, pos.absoluteY);
142
+ else
143
+ el = el.parentNode;
144
+ }
145
+
146
+ return false;
115
147
  },
116
148
 
117
- mouseup: function(index) {
118
- var mouseupEvent = document.createEvent('MouseEvents');
119
- mouseupEvent.initMouseEvent('mouseup', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
120
- this.nodes[index].dispatchEvent(mouseupEvent);
149
+ clickPosition: function(node) {
150
+ var rects = node.getClientRects();
151
+ var rect;
152
+
153
+ for (var i = 0; i < rects.length; i++) {
154
+ rect = rects[i];
155
+ if (rect.width > 0 && rect.height > 0)
156
+ return CapybaraInvocation.clickPosition(node, rect.left, rect.top, rect.width, rect.height);
157
+ }
121
158
  },
122
159
 
123
- click: function (index) {
124
- this.mousedown(index);
125
- this.focus(index);
126
- this.mouseup(index);
127
- var clickEvent = document.createEvent('MouseEvents');
128
- clickEvent.initMouseEvent('click', true, true, window, 0, 0, 0, 0, 0, false, false, false, false, 0, null);
129
- this.nodes[index].dispatchEvent(clickEvent);
160
+ click: function (index, action) {
161
+ var node = this.nodes[index];
162
+ node.scrollIntoViewIfNeeded();
163
+
164
+ var pos = this.clickPosition(node);
165
+
166
+ if (pos && this.clickTest(node, pos))
167
+ action(pos.absoluteX, pos.absoluteY);
168
+ else
169
+ throw new Capybara.ClickFailed(this.path(index), pos);
130
170
  },
131
171
 
132
- trigger: function (index, eventName) {
133
- var eventObject = document.createEvent("HTMLEvents");
134
- eventObject.initEvent(eventName, true, true);
135
- this.nodes[index].dispatchEvent(eventObject);
172
+ leftClick: function (index) {
173
+ this.click(index, CapybaraInvocation.leftClick);
136
174
  },
137
175
 
138
- keypress: function(index, altKey, ctrlKey, shiftKey, metaKey, keyCode, charCode) {
139
- var eventObject = document.createEvent("Events");
140
- eventObject.initEvent('keypress', true, true);
141
- eventObject.window = window;
142
- eventObject.altKey = altKey;
143
- eventObject.ctrlKey = ctrlKey;
144
- eventObject.shiftKey = shiftKey;
145
- eventObject.metaKey = metaKey;
146
- eventObject.keyCode = keyCode;
147
- eventObject.charCode = charCode;
148
- eventObject.which = keyCode;
149
- this.nodes[index].dispatchEvent(eventObject);
176
+ doubleClick: function(index) {
177
+ this.click(index, CapybaraInvocation.leftClick);
178
+ this.click(index, CapybaraInvocation.doubleClick);
179
+ },
180
+
181
+ rightClick: function(index) {
182
+ this.click(index, CapybaraInvocation.rightClick);
150
183
  },
151
184
 
152
- keyupdown: function(index, eventName, keyCode) {
185
+ hover: function (index) {
186
+ var node = this.nodes[index];
187
+ node.scrollIntoViewIfNeeded();
188
+
189
+ var pos = this.clickPosition(node);
190
+ if (pos)
191
+ CapybaraInvocation.hover(pos.absoluteX, pos.absoluteY);
192
+ },
193
+
194
+ trigger: function (index, eventName) {
153
195
  var eventObject = document.createEvent("HTMLEvents");
154
196
  eventObject.initEvent(eventName, true, true);
155
- eventObject.keyCode = keyCode;
156
- eventObject.which = keyCode;
157
- eventObject.charCode = 0;
158
197
  this.nodes[index].dispatchEvent(eventObject);
159
198
  },
160
199
 
@@ -187,49 +226,6 @@ Capybara = {
187
226
  return true;
188
227
  },
189
228
 
190
- characterToKeyCode: function(character) {
191
- var code = character.toUpperCase().charCodeAt(0);
192
- var specialKeys = {
193
- 96: 192, //`
194
- 45: 189, //-
195
- 61: 187, //=
196
- 91: 219, //[
197
- 93: 221, //]
198
- 92: 220, //\
199
- 59: 186, //;
200
- 39: 222, //'
201
- 44: 188, //,
202
- 46: 190, //.
203
- 47: 191, ///
204
- 127: 46, //delete
205
- 126: 192, //~
206
- 33: 49, //!
207
- 64: 50, //@
208
- 35: 51, //#
209
- 36: 52, //$
210
- 37: 53, //%
211
- 94: 54, //^
212
- 38: 55, //&
213
- 42: 56, //*
214
- 40: 57, //(
215
- 41: 48, //)
216
- 95: 189, //_
217
- 43: 187, //+
218
- 123: 219, //{
219
- 125: 221, //}
220
- 124: 220, //|
221
- 58: 186, //:
222
- 34: 222, //"
223
- 60: 188, //<
224
- 62: 190, //>
225
- 63: 191 //?
226
- };
227
- if (specialKeys[code]) {
228
- code = specialKeys[code];
229
- }
230
- return code;
231
- },
232
-
233
229
  set: function (index, value) {
234
230
  var length, maxLength, node, strindex, textTypes, type;
235
231
 
@@ -247,25 +243,21 @@ Capybara = {
247
243
  length = value.length;
248
244
  }
249
245
 
250
- node.value = "";
246
+ if (!node.readOnly)
247
+ node.value = "";
248
+
251
249
  for (strindex = 0; strindex < length; strindex++) {
252
- node.value += value[strindex];
253
- var keyCode = this.characterToKeyCode(value[strindex]);
254
- this.keyupdown(index, "keydown", keyCode);
255
- this.keypress(index, false, false, false, false, value.charCodeAt(strindex), value.charCodeAt(strindex));
256
- this.keyupdown(index, "keyup", keyCode);
257
- this.trigger(index, "input");
250
+ CapybaraInvocation.keypress(value[strindex]);
258
251
  }
259
- this.trigger(index, "change");
260
252
 
261
253
  } else if (type === "checkbox" || type === "radio") {
262
254
  if (node.checked != (value === "true")) {
263
- this.click(index);
255
+ this.leftClick(index);
264
256
  }
265
257
 
266
258
  } else if (type === "file") {
267
259
  this.attachedFiles = Array.prototype.slice.call(arguments, 1);
268
- this.click(index);
260
+ this.leftClick(index);
269
261
 
270
262
  } else {
271
263
  node.value = value;
@@ -286,7 +278,7 @@ Capybara = {
286
278
  this.trigger(index, "change");
287
279
  },
288
280
 
289
- centerPostion: function(element) {
281
+ centerPosition: function(element) {
290
282
  this.reflow(element);
291
283
  var rect = element.getBoundingClientRect();
292
284
  var position = {
@@ -320,7 +312,7 @@ Capybara = {
320
312
 
321
313
  dragTo: function (index, targetIndex) {
322
314
  var element = this.nodes[index], target = this.nodes[targetIndex];
323
- var position = this.centerPostion(element);
315
+ var position = this.centerPosition(element);
324
316
  var options = {
325
317
  clientX: position.x,
326
318
  clientY: position.y
@@ -335,7 +327,7 @@ Capybara = {
335
327
  options.clientY += 1;
336
328
  mouseTrigger('mousemove', options);
337
329
 
338
- position = this.centerPostion(target);
330
+ position = this.centerPosition(target);
339
331
  options = {
340
332
  clientX: position.x,
341
333
  clientY: position.y
@@ -349,3 +341,13 @@ Capybara = {
349
341
  }
350
342
  };
351
343
 
344
+ Capybara.ClickFailed = function(path, position) {
345
+ this.name = 'Capybara.ClickFailed';
346
+ this.message = 'Failed to click element ' + path;
347
+ if (position)
348
+ this.message += ' at position ' + position["absoluteX"] + ', ' + position["absoluteY"];
349
+ else
350
+ this.message += ' at unknown position';
351
+ };
352
+ Capybara.ClickFailed.prototype = new Error();
353
+ Capybara.ClickFailed.prototype.constructor = Capybara.ClickFailed;
data/src/find_command.h CHANGED
@@ -4,7 +4,7 @@
4
4
  }
5
5
 
6
6
  CHECK_COMMAND(Visit)
7
- CHECK_COMMAND(Find)
7
+ CHECK_COMMAND(FindXpath)
8
8
  CHECK_COMMAND(Reset)
9
9
  CHECK_COMMAND(Node)
10
10
  CHECK_COMMAND(Evaluate)
@@ -39,4 +39,6 @@ CHECK_COMMAND(JavascriptPromptMessages)
39
39
  CHECK_COMMAND(GetTimeout)
40
40
  CHECK_COMMAND(SetTimeout)
41
41
  CHECK_COMMAND(SetUrlBlacklist)
42
-
42
+ CHECK_COMMAND(Title)
43
+ CHECK_COMMAND(Version)
44
+ CHECK_COMMAND(FindCss)
data/src/main.cpp CHANGED
@@ -1,5 +1,5 @@
1
1
  #include "Server.h"
2
- #include <QtGui>
2
+ #include <QApplication>
3
3
  #include <iostream>
4
4
  #ifdef Q_OS_UNIX
5
5
  #include <unistd.h>
data/src/stable.h ADDED
@@ -0,0 +1,39 @@
1
+ #include <QApplication>
2
+ #include <QByteArray>
3
+ #include <QDebug>
4
+ #include <QEvent>
5
+ #include <QFile>
6
+ #include <QIODevice>
7
+ #include <QList>
8
+ #include <QNetworkCookie>
9
+ #include <QNetworkProxy>
10
+ #include <QNetworkReply>
11
+ #include <QNetworkRequest>
12
+ #include <QObject>
13
+ #include <QResource>
14
+ #include <QSet>
15
+ #include <QString>
16
+ #include <QStringList>
17
+ #include <QTcpServer>
18
+ #include <QTcpSocket>
19
+ #include <QTimer>
20
+ #include <QUuid>
21
+ #include <QVariant>
22
+ #include <QVariantList>
23
+ #include <QWebElement>
24
+ #include <QWebSettings>
25
+ #include <QtNetwork/QNetworkAccessManager>
26
+ #include <QtNetwork/QNetworkCookie>
27
+ #include <QtNetwork/QNetworkCookieJar>
28
+ #include <QtNetwork/QNetworkReply>
29
+ #include <QtNetwork/QNetworkRequest>
30
+ #include <QtNetwork>
31
+ #if QT_VERSION >= QT_VERSION_CHECK(5, 0, 0)
32
+ #include <QtWebKitWidgets>
33
+ #else
34
+ #include <QtWebKit>
35
+ #endif
36
+ #include <cmath>
37
+ #include <fstream>
38
+ #include <iostream>
39
+ #include <sstream>
@@ -2,6 +2,7 @@ TEMPLATE = app
2
2
  TARGET = webkit_server
3
3
  DESTDIR = .
4
4
  HEADERS = \
5
+ Version.h \
5
6
  EnableLogging.h \
6
7
  Authenticate.h \
7
8
  SetConfirmAction.h \
@@ -21,7 +22,6 @@ HEADERS = \
21
22
  Command.h \
22
23
  SocketCommand.h \
23
24
  Visit.h \
24
- Find.h \
25
25
  Reset.h \
26
26
  Node.h \
27
27
  JavascriptInvocation.h \
@@ -55,9 +55,17 @@ HEADERS = \
55
55
  TimeoutCommand.h \
56
56
  SetUrlBlacklist.h \
57
57
  NoOpReply.h \
58
- JsonSerializer.h
58
+ JsonSerializer.h \
59
+ InvocationResult.h \
60
+ ErrorMessage.h \
61
+ Title.h \
62
+ FindCss.h \
63
+ JavascriptCommand.h \
64
+ FindXpath.h \
65
+ NetworkReplyProxy.h
59
66
 
60
67
  SOURCES = \
68
+ Version.cpp \
61
69
  EnableLogging.cpp \
62
70
  Authenticate.cpp \
63
71
  SetConfirmAction.cpp \
@@ -78,7 +86,6 @@ SOURCES = \
78
86
  Command.cpp \
79
87
  SocketCommand.cpp \
80
88
  Visit.cpp \
81
- Find.cpp \
82
89
  Reset.cpp \
83
90
  Node.cpp \
84
91
  JavascriptInvocation.cpp \
@@ -112,10 +119,23 @@ SOURCES = \
112
119
  TimeoutCommand.cpp \
113
120
  SetUrlBlacklist.cpp \
114
121
  NoOpReply.cpp \
115
- JsonSerializer.cpp
122
+ JsonSerializer.cpp \
123
+ InvocationResult.cpp \
124
+ ErrorMessage.cpp \
125
+ Title.cpp \
126
+ FindCss.cpp \
127
+ JavascriptCommand.cpp \
128
+ FindXpath.cpp \
129
+ NetworkReplyProxy.cpp
116
130
 
117
131
  RESOURCES = webkit_server.qrc
118
- QT += network webkit
119
- CONFIG += console
132
+ QT += network
133
+ greaterThan(QT_MAJOR_VERSION, 4) {
134
+ QT += webkitwidgets
135
+ } else {
136
+ QT += webkit
137
+ }
138
+ CONFIG += console precompile_header
120
139
  CONFIG -= app_bundle
140
+ PRECOMPILED_HEADER = stable.h
121
141