browser_check.py 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. # -*- coding: utf-8 -*-
  2. """
  3. This module is meant to run JupyterLab in a headless browser, making sure
  4. the application launches and starts up without errors.
  5. """
  6. from concurrent.futures import ThreadPoolExecutor
  7. import logging
  8. from os import path as osp
  9. import os
  10. import shutil
  11. import sys
  12. import subprocess
  13. from tornado.ioloop import IOLoop
  14. from notebook.notebookapp import flags, aliases
  15. from traitlets import Bool
  16. from .labapp import LabApp, get_app_dir
  17. here = osp.abspath(osp.dirname(__file__))
  18. test_flags = dict(flags)
  19. test_flags['core-mode'] = (
  20. {'BrowserApp': {'core_mode': True}},
  21. "Start the app in core mode."
  22. )
  23. test_flags['dev-mode'] = (
  24. {'BrowserApp': {'dev_mode': True}},
  25. "Start the app in dev mode."
  26. )
  27. test_aliases = dict(aliases)
  28. test_aliases['app-dir'] = 'BrowserApp.app_dir'
  29. class LogErrorHandler(logging.Handler):
  30. """A handler that exits with 1 on a logged error."""
  31. def __init__(self):
  32. super().__init__(level=logging.ERROR)
  33. self.errored = False
  34. def filter(self, record):
  35. # known startup error message
  36. if 'paste' in record.msg:
  37. return
  38. return super().filter(record)
  39. def emit(self, record):
  40. print(record.msg, file=sys.stderr)
  41. self.errored = True
  42. def run_test(app, func):
  43. """Run a test against the application.
  44. func is a function that accepts an app url as a parameter and returns a result.
  45. """
  46. handler = LogErrorHandler()
  47. def finished(future):
  48. try:
  49. result = future.result()
  50. except Exception as e:
  51. self.log.error(str(e))
  52. if handler.errored:
  53. sys.exit(1)
  54. else:
  55. sys.exit(result)
  56. app.log.addHandler(handler)
  57. pool = ThreadPoolExecutor()
  58. future = pool.submit(run_browser, app.display_url)
  59. IOLoop.current().add_future(future, finished)
  60. class BrowserApp(LabApp):
  61. """An app the launches JupyterLab and waits for it to start up, checking for
  62. JS console errors, JS errors, and Python logged errors.
  63. """
  64. open_browser = Bool(False)
  65. base_url = '/foo/'
  66. ip = '127.0.0.1'
  67. flags = test_flags
  68. aliases = test_aliases
  69. def start(self):
  70. web_app = self.web_app
  71. web_app.settings.setdefault('page_config_data', dict())
  72. web_app.settings['page_config_data']['browserTest'] = True
  73. web_app.settings['page_config_data']['buildAvailable'] = False
  74. run_test(self, run_browser)
  75. super().start()
  76. def run_browser(url):
  77. """Run the browser test and return an exit code.
  78. """
  79. target = osp.join(get_app_dir(), 'browser_test')
  80. if not osp.exists(osp.join(target, 'node_modules')):
  81. os.makedirs(target)
  82. subprocess.call(["jlpm"], cwd=target)
  83. subprocess.call(["jlpm", "add", "puppeteer"], cwd=target)
  84. shutil.copy(osp.join(here, 'chrome-test.js'), osp.join(target, 'chrome-test.js'))
  85. return subprocess.check_call(["node", "chrome-test.js", url], cwd=target)
  86. if __name__ == '__main__':
  87. BrowserApp.launch_instance()