From ed7c879997a13661a073b5180f4d3879fca6fc31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Krassowski?= <5832902+krassowski@users.noreply.github.com> Date: Sat, 15 Apr 2023 09:18:48 +0100 Subject: [PATCH] Align notebook trust behaviour with trust in classic Notebook (#14345) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Trust cells created automatically or by user * Trust code cell after clearing outputs * Clear `trusted` metadata when changing cell type to markdown/raw * Only count code cells in trust indicator. This is because in the current Jupyter trust model only cells with outputs can be marked as (un)trusted. Further, since nbformat does not like `trusted` metadata on non-code cells we have to remove it, and on our side `.trusted` is always undefined for non-code cells. * Add tests for trust on actions, correct implementation for trust change when clearing outputs * Remove unused trust indicator code, avoid DOM modification noise * Add galata test for trust * Add another galata test, document trust mechanism * Update Playwright Snapshots * Use reload with `waitForIsReady: false` see https://github.com/jupyterlab/jupyterlab/issues/14349 * Fix typos (suggestions from code review) Co-authored-by: Frédéric Collonval --------- Co-authored-by: github-actions[bot] Co-authored-by: Frédéric Collonval --- docs/source/user/notebook.rst | 48 ++++++++++- galata/test/documentation/general.test.ts | 31 +++++++ ...tebook-not-trusted-documentation-linux.png | Bin 0 -> 245 bytes .../notebook-trusted-documentation-linux.png | Bin 0 -> 257 bytes .../test/jupyterlab/notebook-create.test.ts | 5 ++ galata/test/jupyterlab/notebook-trust.test.ts | 76 +++++++++++++++++ packages/cells/src/model.ts | 2 + packages/notebook/src/actions.tsx | 74 ++++++++++++++-- packages/notebook/src/truststatus.tsx | 80 +++++++++--------- packages/notebook/src/widget.ts | 19 ++++- packages/notebook/test/actions.spec.ts | 38 ++++++++- 11 files changed, 319 insertions(+), 54 deletions(-) create mode 100644 galata/test/documentation/general.test.ts-snapshots/notebook-not-trusted-documentation-linux.png create mode 100644 galata/test/documentation/general.test.ts-snapshots/notebook-trusted-documentation-linux.png create mode 100644 galata/test/jupyterlab/notebook-trust.test.ts diff --git a/docs/source/user/notebook.rst b/docs/source/user/notebook.rst index a25ac78366d0..e7a157a986cd 100644 --- a/docs/source/user/notebook.rst +++ b/docs/source/user/notebook.rst @@ -153,8 +153,48 @@ and select “New Console for Notebook”: .. _cell-toolbar: +Cell Toolbar +^^^^^^^^^^^^ + If there is enough room for it, each cell has a toolbar that provides quick access to -commonly-used functions. If you would like to disable the cell toolbar, run -``jupyter labextension disable @jupyterlab/cell-toolbar-extension`` on the command line. -You can enable it again by running -``jupyter labextension enable @jupyterlab/cell-toolbar-extension``. +commonly-used functions. If you would like to disable the cell toolbar, run: + +.. code:: bash + + jupyter labextension disable @jupyterlab/cell-toolbar-extension + +on the command line. You can enable it again by running: + +.. code:: bash + + jupyter labextension enable @jupyterlab/cell-toolbar-extension + +.. _notebook-trust: + +Trust +^^^^^ + +JavaScript and HTML in notebooks created on other machines are not trusted, +which results in sanitization of HTML and interactive outputs not being +displayed until the notebook is explicitly trusted. + +.. |trusted| image:: ../images/notebook-trusted.png +.. |not-trusted| image:: ../images/notebook-not-trusted.png + +The trust status of the active notebook is indicated by a shield icon in the +status bar; a checkmark (|trusted|) in the shield indicates a trusted +notebook while a cross (|not-trusted|) indicates an untrusted notebook. +To trust a notebook (and render any blocked outputs) use the ``Trust Notebook`` +command available in the :ref:`command palette `. + +JupyterLab follows the Jupyter Notebook's +`Security Model `__ +where any output generated by the current user is trusted, with following +implementation details of relevance to advanced users: + +1. manually re-running a non-trusted cell will mark it as trusted, +2. if any of the code cells is not trusted, the entire notebook is considered + not trusted and none of the outputs will be trusted upon reopening it (while + it is unusual to see a notebook with a single untrusted cell, this can occur + when copy-pasting cells from an untrusted notebook), +3. only code cells can be trusted; the Markdown cells are always sanitised. diff --git a/galata/test/documentation/general.test.ts b/galata/test/documentation/general.test.ts index 21ce85ae7fde..b25982f2eba1 100644 --- a/galata/test/documentation/general.test.ts +++ b/galata/test/documentation/general.test.ts @@ -461,6 +461,37 @@ test.describe('General', () => { }); }); + test('Trust indicator', async ({ page }) => { + await page.goto(); + // Open Data.ipynb which is not trusted by default + await page.dblclick( + '[aria-label="File Browser Section"] >> text=notebooks' + ); + await page.dblclick('text=Data.ipynb'); + + const trustIndictor = page.locator('.jp-StatusItem-trust'); + + expect(await trustIndictor.screenshot()).toMatchSnapshot( + 'notebook_not_trusted.png' + ); + + // Open trust dialog + // Note: we do not `await` here as it only resolves once dialog is closed + const trustPromise = page.evaluate(() => { + return window.jupyterapp.commands.execute('notebook:trust'); + }); + const dialogSelector = '.jp-Dialog-content'; + await page.waitForSelector(dialogSelector); + // Accept option to trust the notebook + await page.click('.jp-Dialog-button.jp-mod-accept'); + // Wait until dialog is gone + await trustPromise; + + expect(await trustIndictor.screenshot()).toMatchSnapshot( + 'notebook_trusted.png' + ); + }); + test('Heading anchor', async ({ page }, testInfo) => { await page.goto(); await setSidebarWidth(page); diff --git a/galata/test/documentation/general.test.ts-snapshots/notebook-not-trusted-documentation-linux.png b/galata/test/documentation/general.test.ts-snapshots/notebook-not-trusted-documentation-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..53b091866e5ed4ef208b3133e9f5437d4f80bd26 GIT binary patch literal 245 zcmeAS@N?(olHy`uVBq!ia0vp^ia;#E0VEg#f1SPtq!^2X+?^QKos)S9e`l}ylmv7p)QxclRe&mW(EZ+<02LC7lj^1M3p;?32v_P*b_ z6K~I1f5eX2_Rgx(!ZS?Mx%w3fvR88Gf83jGsc^MLy;L>VP$=5hC*?>|bB43eyp#j6 zQ%s(8c_?I>+jzda&=sI^OMzeKiON-*JyR4;HO%o#pVOLTqB^ttORtadF{e)ji`HJ* toN!ri@3z^&{2p6QfAii{|6}9&BiyeGR)sB&;JpR%kEg4j%Q~loCIDaOWN82Z literal 0 HcmV?d00001 diff --git a/galata/test/documentation/general.test.ts-snapshots/notebook-trusted-documentation-linux.png b/galata/test/documentation/general.test.ts-snapshots/notebook-trusted-documentation-linux.png new file mode 100644 index 0000000000000000000000000000000000000000..ff447e49af5af0a12258562da6d382ac776d87d8 GIT binary patch literal 257 zcmV+c0sj7pP)Px#yGcYrR4C8Q(xD0hK@^2ypY7xYH4Qf1Vp;Qa&1;kev5ZM%43;gX?M7w5IKlQm zgGCWxR>7h-&4rH>&YcdQrt=GNoXw7JL~yyXOu3I~QRR~L;0tL|rGyF>A6Z5T5!KvZ z7idzVd5GQc{9Il$9Gqa^--1hJFtRY-9C{1Dj#8q`0;}#|4uq^siJ%FrmjH!AvawPQ zu7Fe7(y3JyAy%?&t;eZMT^mhf`{imQnfm?pz5k0p_LaB+dV#U3xCL1T00000NkvXX Hu0mjfrwwZr literal 0 HcmV?d00001 diff --git a/galata/test/jupyterlab/notebook-create.test.ts b/galata/test/jupyterlab/notebook-create.test.ts index 12e4eb25f5d4..e374d3060ff6 100644 --- a/galata/test/jupyterlab/notebook-create.test.ts +++ b/galata/test/jupyterlab/notebook-create.test.ts @@ -4,6 +4,7 @@ import { expect, IJupyterLabPageFixture, test } from '@jupyterlab/galata'; const fileName = 'notebook.ipynb'; +const TRUSTED_SELECTOR = 'svg[data-icon="ui-components:trusted"]'; const menuPaths = ['File', 'Edit', 'View', 'Run', 'Kernel', 'Help']; @@ -25,6 +26,7 @@ test.describe('Notebook Create', () => { await page.notebook.setCell(0, 'raw', 'Just a raw cell'); expect(await page.notebook.getCellCount()).toBe(1); expect(await page.notebook.getCellType(0)).toBe('raw'); + await expect(page.locator(TRUSTED_SELECTOR)).toHaveCount(1); }); test('Create a Markdown cell', async ({ page }) => { @@ -35,12 +37,14 @@ test.describe('Notebook Create', () => { await page.notebook.runCell(1, true); expect(await page.notebook.getCellCount()).toBe(2); expect(await page.notebook.getCellType(1)).toBe('markdown'); + await expect(page.locator(TRUSTED_SELECTOR)).toHaveCount(1); }); test('Create a Code cell', async ({ page }) => { await page.notebook.addCell('code', '2 ** 3'); expect(await page.notebook.getCellCount()).toBe(2); expect(await page.notebook.getCellType(1)).toBe('code'); + await expect(page.locator(TRUSTED_SELECTOR)).toHaveCount(1); }); test('Save Notebook', async ({ page }) => { @@ -72,6 +76,7 @@ test.describe('Notebook Create', () => { const imageName = 'run-cells.png'; expect((await page.notebook.getCellTextOutput(2))[0]).toBe('8'); + await expect(page.locator(TRUSTED_SELECTOR)).toHaveCount(1); const nbPanel = await page.notebook.getNotebookInPanel(); diff --git a/galata/test/jupyterlab/notebook-trust.test.ts b/galata/test/jupyterlab/notebook-trust.test.ts new file mode 100644 index 000000000000..776ffd8cbd51 --- /dev/null +++ b/galata/test/jupyterlab/notebook-trust.test.ts @@ -0,0 +1,76 @@ +// Copyright (c) Jupyter Development Team. +// Distributed under the terms of the Modified BSD License. + +import { expect, test } from '@jupyterlab/galata'; + +const fileName = 'trust.ipynb'; +const TRUSTED_SELECTOR = 'svg[data-icon="ui-components:trusted"]'; +const NOT_TRUSTED_SELECTOR = 'svg[data-icon="ui-components:not-trusted"]'; + +test.describe('Notebook Trust', () => { + test.beforeEach(async ({ page }) => { + await page.notebook.createNew(fileName); + }); + + test('Blank Markdown cell does not break trust', async ({ page }) => { + // See https://github.com/jupyterlab/jupyterlab/issues/9765 + + // Add an empty Markdown cell + await page.notebook.addCell('markdown', ''); + // The notebook should be trusted + await expect(page.locator(TRUSTED_SELECTOR)).toHaveCount(1); + await page.notebook.save(); + // Reload page + await page.reload({ waitForIsReady: false }); + // Should still be trusted + await expect(page.locator(TRUSTED_SELECTOR)).toHaveCount(1); + }); + + test('Trust is lost after manually editing notebook', async ({ page }) => { + const browserContext = page.context(); + await browserContext.grantPermissions(['clipboard-read']); + // Add text to first cell + await page.notebook.setCell(0, 'code', 'TEST_TEXT'); + await page.notebook.save(); + // The notebook should be trusted + await expect(page.locator(TRUSTED_SELECTOR)).toHaveCount(1); + await expect(page.locator(NOT_TRUSTED_SELECTOR)).toHaveCount(0); + + // Open notebook in text editor using context menu + await page.click(`.jp-DirListing-item span:has-text("${fileName}")`, { + button: 'right' + }); + await page.hover('text=Open With'); + await page.click('.lm-Menu li[role="menuitem"]:has-text("Editor")'); + const editorContent = await page.waitForSelector( + '.jp-FileEditor .cm-content' + ); + await editorContent.waitForSelector('text=TEST_TEXT'); + const originalContent = await page.evaluate(async () => { + await window.jupyterapp.commands.execute('fileeditor:select-all'); + await window.jupyterapp.commands.execute('fileeditor:cut'); + return navigator.clipboard.readText(); + }); + const newContent = originalContent.replace('TEST_TEXT', 'SUBSTITUTED_TEXT'); + await page.evaluate( + async ([newContent]) => { + await window.jupyterapp.commands.execute( + 'fileeditor:replace-selection', + { text: newContent } + ); + // Save file after changes + await window.jupyterapp.commands.execute('docmanager:save'); + // Close the file editor view of the notebook + await window.jupyterapp.commands.execute('application:close'); + }, + [newContent] + ); + + // Reload page + await page.reload({ waitForIsReady: false }); + + // It should no longer be trusted + await expect(page.locator(TRUSTED_SELECTOR)).toHaveCount(0); + await expect(page.locator(NOT_TRUSTED_SELECTOR)).toHaveCount(1); + }); +}); diff --git a/packages/cells/src/model.ts b/packages/cells/src/model.ts index 512c94dad664..91383ad3f957 100644 --- a/packages/cells/src/model.ts +++ b/packages/cells/src/model.ts @@ -663,6 +663,8 @@ export class CodeCellModel extends CellModel implements ICodeCellModel { this.executionCount = null; this._setDirty(false); this.sharedModel.deleteMetadata('execution'); + // We trust this cell as it no longer has any outputs. + this.trusted = true; } /** diff --git a/packages/notebook/src/actions.tsx b/packages/notebook/src/actions.tsx index 02ba9edf70e7..09d32ca8294e 100644 --- a/packages/notebook/src/actions.tsx +++ b/packages/notebook/src/actions.tsx @@ -301,6 +301,10 @@ export namespace NotebookActions { const primaryModel = primary.model.sharedModel; const { cell_type, metadata } = primaryModel.toJSON(); + if (primaryModel.cell_type === 'code') { + // We can trust this cell because the outputs will be removed. + metadata.trusted = true; + } const newModel = { cell_type, metadata, @@ -373,7 +377,14 @@ export namespace NotebookActions { const newIndex = notebook.activeCell ? notebook.activeCellIndex : 0; model.sharedModel.insertCell(newIndex, { - cell_type: notebook.notebookConfig.defaultCell + cell_type: notebook.notebookConfig.defaultCell, + metadata: + notebook.notebookConfig.defaultCell === 'code' + ? { + // This is an empty cell created by user, thus is trusted + trusted: true + } + : {} }); // Make the newly inserted cell active. notebook.activeCellIndex = newIndex; @@ -403,7 +414,14 @@ export namespace NotebookActions { const newIndex = notebook.activeCell ? notebook.activeCellIndex + 1 : 0; model.sharedModel.insertCell(newIndex, { - cell_type: notebook.notebookConfig.defaultCell + cell_type: notebook.notebookConfig.defaultCell, + metadata: + notebook.notebookConfig.defaultCell === 'code' + ? { + // This is an empty cell created by user, thus is trusted + trusted: true + } + : {} }); // Make the newly inserted cell active. notebook.activeCellIndex = newIndex; @@ -561,7 +579,14 @@ export namespace NotebookActions { // Do not use push here, as we want an widget insertion // to make sure no placeholder widget is rendered. model.sharedModel.insertCell(notebook.widgets.length, { - cell_type: notebook.notebookConfig.defaultCell + cell_type: notebook.notebookConfig.defaultCell, + metadata: + notebook.notebookConfig.defaultCell === 'code' + ? { + // This is an empty cell created by user, thus is trusted + trusted: true + } + : {} }); notebook.activeCellIndex++; if (notebook.activeCell?.inViewport === false) { @@ -614,7 +639,14 @@ export namespace NotebookActions { ); const model = notebook.model; model.sharedModel.insertCell(notebook.activeCellIndex + 1, { - cell_type: notebook.notebookConfig.defaultCell + cell_type: notebook.notebookConfig.defaultCell, + metadata: + notebook.notebookConfig.defaultCell === 'code' + ? { + // This is an empty cell created by user, thus is trusted + trusted: true + } + : {} }); notebook.activeCellIndex++; if (notebook.activeCell?.inViewport === false) { @@ -2371,13 +2403,26 @@ namespace Private { const cells = notebook.model!.cells; const index = findIndex(cells, model => model === cell.model); + // While this cell has no outputs and could be trusted following the letter + // of Jupyter trust model, its content comes from kernel and hence is not + // necessarily controlled by the user; if we set it as trusted, a user + // executing cells in succession could end up with unwanted trusted output. if (index === -1) { notebookModel.insertCell(notebookModel.cells.length, { cell_type: 'code', - source: text + source: text, + metadata: { + trusted: false + } }); } else { - notebookModel.insertCell(index + 1, { cell_type: 'code', source: text }); + notebookModel.insertCell(index + 1, { + cell_type: 'code', + source: text, + metadata: { + trusted: false + } + }); } } @@ -2460,6 +2505,14 @@ namespace Private { const raw = child.model.toJSON(); notebookSharedModel.transact(() => { notebookSharedModel.deleteCell(index); + if (value === 'code') { + // After change of type outputs are deleted so cell can be trusted. + raw.metadata.trusted = true; + } else { + // Otherwise clear the metadata as trusted is only "valid" on code + // cells (since other cell types cannot have outputs). + raw.metadata.trusted = undefined; + } const newCell = notebookSharedModel.insertCell(index, { cell_type: value, source: raw.source, @@ -2522,7 +2575,14 @@ namespace Private { // a notebook's last cell undoable. if (sharedModel.cells.length == toDelete.length) { sharedModel.insertCell(0, { - cell_type: notebook.notebookConfig.defaultCell + cell_type: notebook.notebookConfig.defaultCell, + metadata: + notebook.notebookConfig.defaultCell === 'code' + ? { + // This is an empty cell created in empty notebook, thus is trusted + trusted: true + } + : {} }); } }); diff --git a/packages/notebook/src/truststatus.tsx b/packages/notebook/src/truststatus.tsx index 615ff02cd48f..ba761e886ace 100644 --- a/packages/notebook/src/truststatus.tsx +++ b/packages/notebook/src/truststatus.tsx @@ -14,43 +14,36 @@ import { import React from 'react'; import { INotebookModel, Notebook } from '.'; +const TRUST_CLASS = 'jp-StatusItem-trust'; + /** * Determine the notebook trust status message. */ function cellTrust( props: NotebookTrustComponent.IProps | NotebookTrustStatus.Model, translator?: ITranslator -): string[] { +): string { translator = translator || nullTranslator; const trans = translator.load('jupyterlab'); if (props.trustedCells === props.totalCells) { - return [ - trans.__( - 'Notebook trusted: %1 of %2 cells trusted.', - props.trustedCells, - props.totalCells - ), - 'jp-StatusItem-trusted' - ]; + return trans.__( + 'Notebook trusted: %1 of %2 code cells trusted.', + props.trustedCells, + props.totalCells + ); } else if (props.activeCellTrusted) { - return [ - trans.__( - 'Active cell trusted: %1 of %2 cells trusted.', - props.trustedCells, - props.totalCells - ), - 'jp-StatusItem-trusted' - ]; + return trans.__( + 'Active cell trusted: %1 of %2 code cells trusted.', + props.trustedCells, + props.totalCells + ); } else { - return [ - trans.__( - 'Notebook not trusted: %1 of %2 cells trusted.', - props.trustedCells, - props.totalCells - ), - 'jp-StatusItem-untrusted' - ]; + return trans.__( + 'Notebook not trusted: %1 of %2 code cells trusted.', + props.trustedCells, + props.totalCells + ); } } @@ -90,12 +83,12 @@ namespace NotebookTrustComponent { activeCellTrusted: boolean; /** - * The total number of cells for the current notebook. + * The total number of code cells for the current notebook. */ totalCells: number; /** - * The number of trusted cells for the current notebook. + * The number of trusted code cells for the current notebook. */ trustedCells: number; } @@ -111,6 +104,7 @@ export class NotebookTrustStatus extends VDomRenderer constructor(translator?: ITranslator) { super(new NotebookTrustStatus.Model()); this.translator = translator || nullTranslator; + this.node.classList.add(TRUST_CLASS); } /** @@ -120,16 +114,17 @@ export class NotebookTrustStatus extends VDomRenderer if (!this.model) { return null; } - this.node.title = cellTrust(this.model, this.translator)[0]; + const newTitle = cellTrust(this.model, this.translator); + if (newTitle !== this.node.title) { + this.node.title = newTitle; + } return ( -
- -
+ ); } @@ -145,14 +140,14 @@ export namespace NotebookTrustStatus { */ export class Model extends VDomModel { /** - * The number of trusted cells in the current notebook. + * The number of trusted code cells in the current notebook. */ get trustedCells(): number { return this._trustedCells; } /** - * The total number of cells in the current notebook. + * The total number of code cells in the current notebook. */ get totalCells(): number { return this._totalCells; @@ -240,7 +235,7 @@ export namespace NotebookTrustStatus { } /** - * Given a notebook model, figure out how many of the cells are trusted. + * Given a notebook model, figure out how many of the code cells are trusted. */ private _deriveCellTrustState(model: INotebookModel | null): { total: number; @@ -249,13 +244,18 @@ export namespace NotebookTrustStatus { if (model === null) { return { total: 0, trusted: 0 }; } + let total = 0; let trusted = 0; for (const cell of model.cells) { + if (cell.type !== 'code') { + continue; + } + total++; if (cell.trusted) { trusted++; } } - return { total: model.cells.length, trusted }; + return { total, trusted }; } /** diff --git a/packages/notebook/src/widget.ts b/packages/notebook/src/widget.ts index f8cabcba5413..a33950666a71 100644 --- a/packages/notebook/src/widget.ts +++ b/packages/notebook/src/widget.ts @@ -548,7 +548,14 @@ export class StaticNotebook extends WindowedList { const collab = newValue.collaborative ?? false; if (!collab && !cells.length) { newValue.sharedModel.insertCell(0, { - cell_type: this.notebookConfig.defaultCell + cell_type: this.notebookConfig.defaultCell, + metadata: + this.notebookConfig.defaultCell === 'code' + ? { + // This is an empty cell created in empty notebook, thus is trusted + trusted: true + } + : {} }); } let index = -1; @@ -599,7 +606,14 @@ export class StaticNotebook extends WindowedList { requestAnimationFrame(() => { if (model && !model.isDisposed && !model.sharedModel.cells.length) { model.sharedModel.insertCell(0, { - cell_type: this.notebookConfig.defaultCell + cell_type: this.notebookConfig.defaultCell, + metadata: + this.notebookConfig.defaultCell === 'code' + ? { + // This is an empty cell created in empty notebook, thus is trusted + trusted: true + } + : {} }); } }); @@ -2498,6 +2512,7 @@ export class Notebook extends StaticNotebook { const start = index; const values = event.mimeData.getData(JUPYTER_CELL_MIME); // Insert the copies of the original cells. + // We preserve trust status of pasted cells by not modifying metadata. model.sharedModel.insertCells(index, values); // Select the inserted cells. this.deselectAll(); diff --git a/packages/notebook/test/actions.spec.ts b/packages/notebook/test/actions.spec.ts index 6cc48277c70e..37daece2b95f 100644 --- a/packages/notebook/test/actions.spec.ts +++ b/packages/notebook/test/actions.spec.ts @@ -301,6 +301,12 @@ describe('@jupyterlab/notebook', () => { expect(cell.model.outputs.length).toBe(0); }); + it('should mark cell as trusted as cells without output are trusted', () => { + NotebookActions.mergeCells(widget); + const cell = widget.activeCell as CodeCell; + expect(cell.model.trusted).toBe(true); + }); + it('should preserve the widget mode', () => { widget.mode = 'edit'; NotebookActions.mergeCells(widget); @@ -468,7 +474,7 @@ describe('@jupyterlab/notebook', () => { expect(widget.activeCell).toBeNull(); }); - it('should widget mode should be preserved', () => { + it('widget mode should be preserved', () => { NotebookActions.insertAbove(widget); expect(widget.mode).toBe('command'); widget.mode = 'edit'; @@ -500,6 +506,11 @@ describe('@jupyterlab/notebook', () => { NotebookActions.insertAbove(widget); expect(widget.activeCell!.model.sharedModel.getSource()).toBe(''); }); + + it('should mark inserted code cell as trusted', () => { + NotebookActions.insertAbove(widget); + expect(widget.activeCell!.model.trusted).toBe(true); + }); }); describe('#insertBelow()', () => { @@ -549,6 +560,11 @@ describe('@jupyterlab/notebook', () => { NotebookActions.insertBelow(widget); expect(widget.activeCell!.model.sharedModel.getSource()).toBe(''); }); + + it('should mark inserted code cell as trusted', () => { + NotebookActions.insertBelow(widget); + expect(widget.activeCell!.model.trusted).toBe(true); + }); }); describe('#changeCellType()', () => { @@ -600,6 +616,21 @@ describe('@jupyterlab/notebook', () => { const cell = widget.activeCell as MarkdownCell; expect(cell.rendered).toBe(false); }); + + it('should mark code cell as trusted', () => { + // Switch to markdown and then to code as otherwise this is no-op. + NotebookActions.changeCellType(widget, 'markdown'); + NotebookActions.changeCellType(widget, 'code'); + const cell = widget.activeCell as CodeCell; + expect(cell.model.trusted).toBe(true); + }); + + it('should clear trust metadata if switching away from code cell', () => { + widget.activeCell!.model.trusted = true; + NotebookActions.changeCellType(widget, 'markdown'); + const cell = widget.activeCell as MarkdownCell; + expect(cell.model.metadata.trusted).toBe(undefined); + }); }); describe('#run()', () => { @@ -818,6 +849,7 @@ describe('@jupyterlab/notebook', () => { expect(result).toBe(true); expect(widget.widgets.length).toBe(count + 1); expect(widget.activeCell).toBeInstanceOf(CodeCell); + expect(widget.activeCell!.model.trusted).toBe(true); expect(widget.mode).toBe('edit'); }); @@ -914,6 +946,7 @@ describe('@jupyterlab/notebook', () => { ); expect(result).toBe(true); expect(widget.activeCell).toBeInstanceOf(CodeCell); + expect(widget.activeCell!.model.trusted).toBe(true); expect(widget.mode).toBe('edit'); expect(widget.widgets.length).toBe(count + 1); }); @@ -1542,8 +1575,10 @@ describe('@jupyterlab/notebook', () => { NotebookActions.clearOutputs(widget); let cell = widget.widgets[0] as CodeCell; expect(cell.model.outputs.length).toBe(0); + expect(cell.model.trusted).toBe(true); cell = widget.widgets[index] as CodeCell; expect(cell.model.outputs.length).toBe(0); + expect(cell.model.trusted).toBe(true); }); it('should preserve the widget mode', () => { @@ -1571,6 +1606,7 @@ describe('@jupyterlab/notebook', () => { if (cell instanceof CodeCell) { // eslint-disable-next-line jest/no-conditional-expect expect(cell.model.outputs.length).toBe(0); + expect(cell.model.trusted).toBe(true); } } });