playwright_server 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226
  1. #!/usr/bin/node
  2. "use strict";
  3. // If we don't have this, we're done for
  4. const { exit } = require('process');
  5. const fs = require('fs');
  6. const path = require('path');
  7. // Don't explode, but be helpful when we have unsatisfied deps
  8. try {
  9. require('yargs');
  10. require('uuid');
  11. require('playwright');
  12. require('express');
  13. } catch {
  14. console.log("Required dependencies are not installed. Cannot continue, run `npm i` to fix.");
  15. exit(1);
  16. }
  17. // Get what we actually want from our deps
  18. const yargs = require('yargs');
  19. const { v4 : uuidv4 } = require('uuid');
  20. const { chromium, firefox, webkit, devices } = require('playwright');
  21. const express = require('express');
  22. // Defines our interface
  23. let sharedir = require.resolve('playwright'); // api.json should be shipped with playwright itself
  24. var theFile = path.dirname(sharedir) + '/api.json';
  25. let rawdata = fs.readFileSync(theFile);
  26. // On newer versions of playwright, instead of using hash tables they just lazily output arrays, so we have to convert this
  27. let spec = JSON.parse(rawdata);
  28. function arr2hash (arr,primary_key) {
  29. var inside_out = {};
  30. for (var item of arr) {
  31. inside_out[item.name] = item;
  32. }
  33. return inside_out;
  34. }
  35. var fix_it=false;
  36. if (spec instanceof Array) {
  37. fix_it = true;
  38. spec = arr2hash(spec,'name');
  39. }
  40. // Establish argument order for callers, and correct spec array-ification
  41. for (var classname of Object.keys(spec)) {
  42. if (spec[classname].members instanceof Array) {
  43. spec[classname].members = arr2hash(spec[classname].members,'name');
  44. }
  45. for (var method of Object.keys(spec[classname].members)) {
  46. var order = 0;
  47. if (spec[classname].members[method].args instanceof Array) {
  48. spec[classname].members[method].args = arr2hash(spec[classname].members[method].args,'name');
  49. }
  50. for (var arg of Object.keys(spec[classname].members[method].args) ) {
  51. spec[classname].members[method].args[arg].order = order++;
  52. }
  53. }
  54. }
  55. //XXX spec is wrong here unfortunately
  56. if (fix_it) {
  57. for (var className of ['Page','Frame']) {
  58. spec[className].members.$$ = spec[className].members.querySelectorAll;
  59. spec[className].members.$ = spec[className].members.querySelector;
  60. spec[className].members.$$eval = spec[className].members.evalOnSelectorAll;
  61. spec[className].members.$eval = spec[className].members.evalOnSelector;
  62. }
  63. }
  64. const argv = yargs
  65. .option('debug', {
  66. alias: 'd',
  67. description: 'Print additional debugging messages',
  68. type: 'boolean',
  69. })
  70. .option('port', {
  71. alias: 'p',
  72. description: 'Run on specified port',
  73. type: 'number',
  74. })
  75. .option('check', {
  76. alias: 'c',
  77. description: 'Print whether our kit is good (or explode)',
  78. type: 'boolean'
  79. })
  80. .help()
  81. .alias('help', 'h')
  82. .argv;
  83. if (argv.check) {
  84. console.log('OK');
  85. exit(0);
  86. }
  87. const app = express();
  88. const port = argv.port || 6969;
  89. var objects = {};
  90. var browsers = { 'firefox' : firefox, 'chrome' : chromium, 'webkit' : webkit };
  91. //Stash for users to put data in
  92. var userdata = {};
  93. app.use(express.json())
  94. app.get('/spec', async (req, res) => {
  95. res.json( { error : false, message : spec } );
  96. });
  97. app.post('/session', async (req, res) => {
  98. var payload = req.body;
  99. var type = payload.type;
  100. var args = payload.args || [];
  101. var result;
  102. if ( type && browsers[type] ) {
  103. try {
  104. var browser = await browsers[type].launch(...args);
  105. objects[browser._guid] = browser;
  106. result = { error : false, message : browser };
  107. } catch (e) {
  108. result = { error : true, message : e.message};
  109. }
  110. } else {
  111. result = { error : true, message : "Please select a supported browser" };
  112. }
  113. res.json(result);
  114. });
  115. app.post('/command', async (req, res) => {
  116. var payload = req.body;
  117. var type = payload.type;
  118. var object = payload.object;
  119. var command = payload.command;
  120. var args = payload.args || [];
  121. var result = {};
  122. if (argv.debug) {
  123. console.log(type,object,command,args);
  124. }
  125. // XXX this would be cleaner if the mouse() and keyboard() methods just returned a Mouse and Keyboard object
  126. var subject = objects[object];
  127. if (subject) {
  128. if (type == 'Mouse') {
  129. subject = objects[object].mouse;
  130. } else if (type == 'Keyboard' ) {
  131. subject = objects[object].keyboard;
  132. }
  133. }
  134. if (subject && spec[type] && spec[type].members[command]) {
  135. try {
  136. //XXX We have to do a bit of 'special' handling for scripts
  137. // This has implications for the type of scripts you can use
  138. if (command == 'evaluate' || command == 'evaluateHandle') {
  139. var toEval = args.shift();
  140. const fun = new Function (toEval);
  141. args = [
  142. fun,
  143. ...args
  144. ];
  145. }
  146. const res = await subject[command](...args);
  147. result = { error : false, message : res };
  148. if (Array.isArray(res)) {
  149. for (var r of res) {
  150. objects[r._guid] = r;
  151. }
  152. }
  153. // XXX videos are special, we have to magic up a guid etc for them
  154. if (command == 'video' && res) {
  155. res._guid = 'Video@' + uuidv4();
  156. res._type = 'Video';
  157. }
  158. // XXX So are FileChooser object unfortunately
  159. if (args[0] == 'filechooser' && res) {
  160. res._guid = 'FileChooser@' + uuidv4();
  161. res._type = 'FileChooser';
  162. }
  163. if (res && res._guid) {
  164. objects[res._guid] = res;
  165. }
  166. } catch (e) {
  167. result = { error : true, message : e.message };
  168. }
  169. // Allow creation of event listeners if we can actually wait for them
  170. } else if (command == 'on' && subject && spec[type].members.waitForEvent ) {
  171. try {
  172. var evt = args.shift();
  173. const cb = new Function (args.shift());
  174. subject.on(evt,cb);
  175. result = { error : false, message : "Listener set up" };
  176. } catch (e) {
  177. result = { error : true, message : e.message };
  178. }
  179. } else {
  180. result = { error : true, message : "No such object, or " + command + " is not a globally recognized command for Playwright" };
  181. }
  182. res.json(result);
  183. });
  184. app.get('/shutdown', async (req, res) => {
  185. res.json( { error: false, message : "Sent kill signal to browser" });
  186. process.exit(0);
  187. });
  188. //Modulino
  189. if (require.main === module) {
  190. app.listen( port, () => {
  191. if (argv.debug) {
  192. console.log(`Listening on port ${port}`);
  193. }
  194. });
  195. }