codesnippet.ts 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426
  1. /*
  2. * Copyright 2018-2022 Elyra Authors
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. describe('Code Snippet tests', () => {
  17. const snippetName = 'test-code-snippet';
  18. beforeEach(() => {
  19. cy.resetJupyterLab();
  20. openCodeSnippetExtension();
  21. });
  22. afterEach(() => {
  23. // delete code-snippet used for testing
  24. cy.exec(`elyra-metadata remove code-snippets --name=${snippetName}`, {
  25. failOnNonZeroExit: false
  26. });
  27. });
  28. it('should open the Code Snippet extension', () => {
  29. // make sure it is rendered properly
  30. cy.get('.elyra-metadata .elyra-metadataHeader').contains('Code Snippets');
  31. // and code-snippet create new button is visible
  32. cy.findByRole('button', { name: /create new code snippet/i }).should(
  33. 'be.visible'
  34. );
  35. });
  36. it('should provide warnings when required fields are not entered properly', () => {
  37. createInvalidCodeSnippet(snippetName);
  38. // Metadata editor should not close
  39. cy.get('.lm-TabBar-tabLabel')
  40. .contains('New Code Snippet')
  41. .should('be.visible');
  42. // Fields marked as required should be highlighted
  43. cy.get(
  44. '.elyra-MuiFormHelperText-root.elyra-Mui-error, .MuiFormHelperText-root.Mui-error'
  45. ).as('required-warnings');
  46. cy.get('@required-warnings').should('have.length', 2);
  47. });
  48. it('should create valid code-snippet', () => {
  49. createValidCodeSnippet(snippetName);
  50. // Metadata editor tab should not be visible
  51. cy.get('.lm-TabBar-tabLabel')
  52. .contains('New Code Snippet')
  53. .should('not.exist');
  54. // Check new code snippet is displayed
  55. getSnippetByName(snippetName);
  56. });
  57. it('should fail to create duplicate Code Snippet', () => {
  58. // create code snippet
  59. createValidCodeSnippet(snippetName);
  60. // tries to create duplicate code snippet
  61. createValidCodeSnippet(snippetName);
  62. // Should display dialog
  63. cy.get('.jp-Dialog-header').contains('Error making request');
  64. // Close dialog
  65. cy.get('button.jp-mod-accept').click();
  66. });
  67. it('should trigger save / submit on pressing enter', () => {
  68. populateCodeSnippetFields(snippetName);
  69. cy.get('.elyra-metadataEditor-form-display_name').type('{enter}');
  70. // Metadata editor tab should not be visible
  71. cy.get('.lm-TabBar-tabLabel')
  72. .contains('New Code Snippet')
  73. .should('not.exist');
  74. // Check new code snippet is displayed
  75. getSnippetByName(snippetName);
  76. });
  77. // Delete snippet
  78. it('should delete existing Code Snippet', () => {
  79. createValidCodeSnippet(snippetName);
  80. cy.wait(500);
  81. getSnippetByName(snippetName);
  82. deleteSnippet(snippetName);
  83. });
  84. // Duplicate snippet
  85. it('should duplicate existing Code Snippet', () => {
  86. createValidCodeSnippet(snippetName);
  87. cy.wait(500);
  88. let snippetRef = getSnippetByName(snippetName);
  89. expect(snippetRef).to.not.be.null;
  90. // create a duplicate of this snippet
  91. duplicateSnippet(snippetName);
  92. cy.wait(100);
  93. snippetRef = getSnippetByName(`${snippetName}-Copy1`);
  94. expect(snippetRef).to.not.be.null;
  95. // create another duplicate of this snippet
  96. duplicateSnippet(snippetName);
  97. cy.wait(100);
  98. snippetRef = getSnippetByName(`${snippetName}-Copy2`);
  99. expect(snippetRef).to.not.be.null;
  100. // cleanup
  101. deleteSnippet(snippetName);
  102. deleteSnippet(`${snippetName}-Copy1`);
  103. deleteSnippet(`${snippetName}-Copy2`);
  104. });
  105. it('should have visible action buttons for existing code snippet', () => {
  106. createValidCodeSnippet(snippetName);
  107. const actionButtons = getActionButtonsElement(snippetName);
  108. const buttonTitles = [
  109. 'Copy to clipboard',
  110. 'Insert',
  111. 'Edit',
  112. 'Duplicate',
  113. 'Delete'
  114. ];
  115. // Check expected buttons to be visible
  116. buttonTitles.forEach((title: string) => {
  117. actionButtons.within(() => {
  118. cy.get(`button[title="${title}"]`).should('be.visible');
  119. });
  120. });
  121. });
  122. it('should display/hide code snippet content on expand/collapse button', () => {
  123. createValidCodeSnippet(snippetName);
  124. // Check new code snippet is displayed
  125. const item = getSnippetByName(snippetName);
  126. // Click on expand button
  127. item
  128. .parentsUntil('.elyra-metadata-item')
  129. .first()
  130. .find('button')
  131. .first()
  132. .click();
  133. // Check code mirror is visible
  134. cy.get('.elyra-expandableContainer-details-visible').should('exist');
  135. // Click on collapse button
  136. item
  137. .parentsUntil('.elyra-metadata-item')
  138. .first()
  139. .find('button')
  140. .first()
  141. .click();
  142. // Check code mirror is not visible
  143. cy.get('.elyra-expandableContainer-details-visible').should('not.exist');
  144. });
  145. it('should update code snippet name after editing it', () => {
  146. createValidCodeSnippet(snippetName);
  147. // Find new snippet in display and click on edit button
  148. getActionButtonsElement(snippetName).within(() => {
  149. cy.get('button[title="Edit"]').click();
  150. });
  151. // Edit snippet name
  152. const newSnippetName = 'new-name';
  153. cy.get('.elyra-metadataEditor-form-display_name')
  154. .find('input')
  155. .clear()
  156. .type(newSnippetName);
  157. saveAndCloseMetadataEditor();
  158. cy.wait(500);
  159. // Check new snippet name is displayed
  160. const updatedSnippetItem = getSnippetByName(newSnippetName);
  161. // Check old snippet name does not exist
  162. expect(updatedSnippetItem.innerText).not.to.eq(snippetName);
  163. // Delete updated code snippet
  164. deleteSnippet(newSnippetName);
  165. });
  166. it('should fail to insert a code snippet into unsupported widget', () => {
  167. createValidCodeSnippet(snippetName);
  168. // Insert snippet into launcher widget
  169. insert(snippetName);
  170. // Check if insertion failed and dismiss dialog
  171. cy.get('.jp-Dialog-header').contains('Error');
  172. cy.get('button.jp-mod-accept').click();
  173. cy.wait(100);
  174. });
  175. it('should insert a python code snippet into python editor', () => {
  176. createValidCodeSnippet(snippetName);
  177. // Open blank python file
  178. cy.createNewScriptEditor('Python');
  179. cy.wait(1500);
  180. // Insert snippet into python editor
  181. insert(snippetName);
  182. // Check if editor has the new code
  183. cy.get('.CodeMirror:visible');
  184. cy.get('span.cm-string').contains(/test/i);
  185. });
  186. it('should fail to insert a java code snippet into python editor', () => {
  187. createValidCodeSnippet(snippetName, 'Java');
  188. // Open blank python file
  189. cy.createNewScriptEditor('Python');
  190. cy.wait(500);
  191. // Insert snippet into python editor
  192. insert(snippetName);
  193. // Check for language mismatch warning
  194. cy.get('.jp-Dialog-header').contains(/warning/i);
  195. // Dismiss the dialog
  196. cy.findByRole('button', { name: /cancel/i }).click();
  197. // Check it did not insert the code
  198. cy.get('.CodeMirror:visible');
  199. cy.get('span.cm-string').should('not.exist');
  200. });
  201. // DEV NOTE: Uncomment the tests below to run them locally
  202. // TODO: Investigate tests below only failing on CI
  203. // Steps: checkCodeMirror, closeTabWithoutSaving
  204. // it('Test inserting a code snippet into a notebook', () => {
  205. // openCodeSnippetExtension();
  206. // clickCreateNewSnippetButton();
  207. // const snippetName = 'test-code-snippet';
  208. // fillMetadaEditorForm(snippetName);
  209. // cy.wait(500);
  210. // // Open blank notebook file
  211. // cy.get(
  212. // '.jp-LauncherCard[data-category="Notebook"][title="Python 3"]:visible'
  213. // ).click();
  214. // cy.wait(500);
  215. // // Check widget is loaded
  216. // cy.get('.CodeMirror:visible');
  217. // insert(snippetName);
  218. // // Check if notebook cell has the new code
  219. // checkCodeMirror();
  220. // // NOTE: Notebook cell is still empty when this test runs on CI
  221. // closeTabWithoutSaving();
  222. // // NOTE: Save dialog isn't visible when this test runs on CI
  223. // });
  224. // it('Test inserting a code snippet into a markdown file', () => {
  225. // openCodeSnippetExtension();
  226. // clickCreateNewSnippetButton();
  227. // const snippetName = 'test-code-snippet';
  228. // fillMetadaEditorForm(snippetName);
  229. // cy.wait(500);
  230. // // Open blank notebook file
  231. // cy.get(
  232. // '.jp-LauncherCard[title="Create a new markdown file"]:visible'
  233. // ).click();
  234. // cy.wait(500);
  235. // // Check widget is loaded
  236. // cy.get('.CodeMirror:visible');
  237. // insert(snippetName);
  238. // // Check if notebook cell has the new code
  239. // checkCodeMirror();
  240. // // Check for language decoration
  241. // cy.get('span.cm-comment')
  242. // .first()
  243. // .contains('Python');
  244. // closeTabWithoutSaving();
  245. // });
  246. });
  247. // ------------------------------
  248. // ----- Utility Functions ------
  249. // ------------------------------
  250. const openCodeSnippetExtension = (): void => {
  251. cy.get('.jp-SideBar [title="Code Snippets"]').click();
  252. cy.get('.jp-SideBar .lm-mod-current[title="Code Snippets"]');
  253. };
  254. const getSnippetByName = (snippetName: string): any => {
  255. return cy.get(`[data-item-id="${snippetName}"]`);
  256. };
  257. const createInvalidCodeSnippet = (snippetName: string): any => {
  258. clickCreateNewSnippetButton();
  259. // Name code snippet
  260. cy.get('.elyra-metadataEditor-form-display_name').type(snippetName);
  261. saveAndCloseMetadataEditor();
  262. };
  263. const populateCodeSnippetFields = (
  264. snippetName: string,
  265. language?: string
  266. ): any => {
  267. clickCreateNewSnippetButton();
  268. // Name code snippet
  269. cy.get('.elyra-metadataEditor-form-display_name').type(snippetName);
  270. // Select python language from dropdown list
  271. editSnippetLanguage(snippetName, language ?? 'Python');
  272. // Add snippet code
  273. cy.get('.CodeMirror .CodeMirror-scroll:visible').type(
  274. 'print("Code Snippet Test")'
  275. );
  276. };
  277. const createValidCodeSnippet = (
  278. snippetName: string,
  279. language?: string
  280. ): any => {
  281. populateCodeSnippetFields(snippetName, language);
  282. saveAndCloseMetadataEditor();
  283. cy.wait(1000);
  284. };
  285. const clickCreateNewSnippetButton = (): void => {
  286. cy.findByRole('button', { name: /create new code snippet/i }).click();
  287. };
  288. const saveAndCloseMetadataEditor = (): void => {
  289. cy.get('.elyra-metadataEditor-saveButton > button:visible').click();
  290. };
  291. const deleteSnippet = (snippetName: string): void => {
  292. // Find element by name
  293. const item = getSnippetByName(snippetName);
  294. // Click on delete button
  295. item.find('button[title="Delete"]').click();
  296. // Confirm action in dialog
  297. cy.get('.jp-Dialog-header').contains(`Delete snippet '${snippetName}'?`);
  298. cy.get('button.jp-mod-accept').click();
  299. };
  300. const duplicateSnippet = (snippetName: string): void => {
  301. // Find element by name
  302. const item = getSnippetByName(snippetName);
  303. // Click duplicate button
  304. item.find('button[title="Duplicate"]').click();
  305. };
  306. const getActionButtonsElement = (snippetName: string): any => {
  307. const actionButtonsElement = getSnippetByName(snippetName).find(
  308. '.elyra-expandableContainer-action-buttons'
  309. );
  310. return actionButtonsElement;
  311. };
  312. const insert = (snippetName: string): void => {
  313. getActionButtonsElement(snippetName).within(() => {
  314. cy.get('button[title="Insert"]').click();
  315. });
  316. cy.wait(500);
  317. };
  318. const editSnippetLanguage = (snippetName: string, lang: string): void => {
  319. cy.get('.elyra-metadataEditor')
  320. .find('.elyra-form-DropDown-item .elyra-MuiOutlinedInput-root')
  321. .first()
  322. .click();
  323. cy.get('.elyra-MuiAutocomplete-listbox')
  324. .contains(`${lang}`)
  325. .click();
  326. };