utils.ts 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. // Copyright (c) Jupyter Development Team.
  2. // Distributed under the terms of the Modified BSD License.
  3. import { URLExt } from '@jupyterlab/coreutils';
  4. import { ElementHandle, Page } from '@playwright/test';
  5. import * as fs from 'fs-extra';
  6. import * as path from 'path';
  7. /**
  8. * Read a file as a base-64 string
  9. *
  10. * @param filePath Local file path
  11. * @returns Base 64 encoded file content
  12. */
  13. export function base64EncodeFile(filePath: string): string {
  14. const content = fs.readFileSync(filePath);
  15. return content.toString('base64');
  16. }
  17. /**
  18. * Private page config data for the Jupyter application.
  19. */
  20. let configData: { [key: string]: string } | null = null;
  21. // Get config data
  22. async function getConfigData(page: Page): Promise<{ [key: string]: string }> {
  23. if (configData) {
  24. return configData;
  25. }
  26. configData = Object.create(null);
  27. const el = await page.$('#jupyter-config-data');
  28. if (!el) {
  29. return {};
  30. }
  31. configData = JSON.parse((await el?.textContent()) ?? '{}');
  32. for (const key in configData) {
  33. // PageConfig expects strings
  34. if (typeof configData[key] !== 'string') {
  35. configData[key] = JSON.stringify(configData[key]);
  36. }
  37. }
  38. return configData!;
  39. }
  40. /**
  41. * Get a url-encoded item from `body.data` and decode it
  42. * We should never have any encoded URLs anywhere else in code
  43. * until we are building an actual request.
  44. */
  45. async function getBodyData(page: Page, key: string): Promise<string> {
  46. const val = await page.evaluate(key => document.body.dataset[key], key);
  47. if (typeof val === 'undefined') {
  48. return '';
  49. }
  50. return decodeURIComponent(val);
  51. }
  52. /**
  53. * Get the Jupyter server base URL stored in the index.html file
  54. *
  55. * @param page Playwright page model
  56. * @returns Base URL
  57. */
  58. export async function getBaseUrl(page: Page): Promise<string> {
  59. return URLExt.normalize((await getOption(page, 'baseUrl')) || '/');
  60. }
  61. /**
  62. * Get the classes of an element
  63. *
  64. * @param element Element handle
  65. * @returns Classes list
  66. */
  67. export async function getElementClassList(
  68. element: ElementHandle
  69. ): Promise<string[]> {
  70. if (!element) {
  71. return [];
  72. }
  73. const className = await element.getProperty('className');
  74. if (className) {
  75. const classNameList = await className.jsonValue();
  76. if (typeof classNameList === 'string') {
  77. return classNameList.split(' ');
  78. }
  79. }
  80. return [];
  81. }
  82. /**
  83. * List the content of a local directory
  84. *
  85. * @param dirPath Local directory path
  86. * @param filePaths List to populate with the directory content
  87. * @returns Content of the directory
  88. */
  89. export function getFilesInDirectory(
  90. dirPath: string,
  91. filePaths?: string[]
  92. ): string[] {
  93. const files = fs.readdirSync(dirPath);
  94. filePaths = filePaths || [];
  95. for (const file of files) {
  96. if (file.startsWith('.')) {
  97. continue;
  98. }
  99. if (fs.statSync(dirPath + '/' + file).isDirectory()) {
  100. filePaths = getFilesInDirectory(dirPath + '/' + file, filePaths);
  101. } else {
  102. filePaths.push(path.join(dirPath, '/', file));
  103. }
  104. }
  105. return filePaths;
  106. }
  107. /**
  108. * Get the value of an option stored in the page config object
  109. *
  110. * @param page Playwright page model
  111. * @param name Option name
  112. * @returns Option value
  113. */
  114. export async function getOption(page: Page, name: string): Promise<string> {
  115. return (await getConfigData(page))[name] ?? (await getBodyData(page, name));
  116. }
  117. /**
  118. * Get the token stored in the page config object
  119. *
  120. * @param page Playwright page model
  121. * @returns Token
  122. */
  123. export async function getToken(page: Page): Promise<string> {
  124. return (
  125. (await getOption(page, 'token')) ||
  126. (await getBodyData(page, 'jupyterApiToken'))
  127. );
  128. }
  129. /**
  130. * Wait for a function to return true until timeout
  131. *
  132. * @param fn Condition
  133. * @param timeout Time out
  134. */
  135. export async function waitForCondition(
  136. fn: () => boolean | Promise<boolean>,
  137. timeout?: number
  138. ): Promise<void> {
  139. return new Promise((resolve, reject) => {
  140. let checkTimer: NodeJS.Timeout | null = null;
  141. let timeoutTimer: NodeJS.Timeout | null = null;
  142. const check = async () => {
  143. checkTimer = null;
  144. if (await Promise.resolve(fn())) {
  145. if (timeoutTimer) {
  146. clearTimeout(timeoutTimer);
  147. }
  148. resolve();
  149. } else {
  150. checkTimer = setTimeout(check, 200);
  151. }
  152. };
  153. void check();
  154. if (timeout) {
  155. timeoutTimer = setTimeout(() => {
  156. timeoutTimer = null;
  157. if (checkTimer) {
  158. clearTimeout(checkTimer);
  159. }
  160. reject(new Error('Timed out waiting for condition to be fulfilled.'));
  161. }, timeout);
  162. }
  163. });
  164. }
  165. /**
  166. * Wait for an element to emit 'transitionend' event.
  167. *
  168. * @param page Playwright page model object
  169. * @param element Element or selector to watch
  170. */
  171. export async function waitForTransition(
  172. page: Page,
  173. element: ElementHandle<Element> | string
  174. ): Promise<void> {
  175. const el = typeof element === 'string' ? await page.$(element) : element;
  176. if (el) {
  177. return page.evaluate(el => {
  178. return new Promise(resolve => {
  179. const onEndHandler = () => {
  180. el.removeEventListener('transitionend', onEndHandler);
  181. resolve();
  182. };
  183. el.addEventListener('transitionend', onEndHandler);
  184. });
  185. }, el);
  186. }
  187. return Promise.reject();
  188. }
  189. // Selector builders
  190. /**
  191. * Get the selector to look for a specific class
  192. *
  193. * @param className Class name
  194. * @returns Selector
  195. */
  196. export function xpContainsClass(className: string): string {
  197. return `contains(concat(" ", normalize-space(@class), " "), " ${className} ")`;
  198. }
  199. /**
  200. * Get the selector to look for a specific activity tab
  201. *
  202. * @param name Activity name
  203. * @returns Selector
  204. */
  205. export function xpBuildActivityTabSelector(name: string): string {
  206. return `//div[${xpContainsClass('jp-Activity')}]/ul/li[${xpContainsClass(
  207. 'lm-TabBar-tab'
  208. )} and ./div[text()="${name}" and ${xpContainsClass('lm-TabBar-tabLabel')}]]`;
  209. }
  210. /**
  211. * Get the selector to look for a specific activity panel
  212. *
  213. * @param id Activity id
  214. * @returns Selector
  215. */
  216. export function xpBuildActivityPanelSelector(id: string): string {
  217. return `//div[@id='${id}' and ${xpContainsClass(
  218. 'jp-Activity'
  219. )} and ${xpContainsClass('lm-DockPanel-widget')}]`;
  220. }
  221. /**
  222. * Get the selector to look for the currently active activity tab
  223. *
  224. * @returns Selector
  225. */
  226. export function xpBuildActiveActivityTabSelector(): string {
  227. return `//div[${xpContainsClass('jp-Activity')}]/ul/li[${xpContainsClass(
  228. 'lm-TabBar-tab'
  229. )} and ${xpContainsClass('lm-mod-current')} and ./div[${xpContainsClass(
  230. 'lm-TabBar-tabLabel'
  231. )}]]`;
  232. }