Просмотр исходного кода

Expose genericsearchprovider exports, add tests,

and use textContent over innerText.
krassowski 4 лет назад
Родитель
Сommit
c8599e111c

+ 1 - 0
packages/documentsearch/babel.config.js

@@ -0,0 +1 @@
+module.exports = require('@jupyterlab/testutils/lib/babel.config');

+ 2 - 0
packages/documentsearch/jest.config.js

@@ -0,0 +1,2 @@
+const func = require('@jupyterlab/testutils/lib/jest-config');
+module.exports = func(__dirname);

+ 9 - 0
packages/documentsearch/package.json

@@ -28,8 +28,13 @@
   ],
   "scripts": {
     "build": "tsc -b",
+    "build:test": "tsc --build tsconfig.test.json",
     "clean": "rimraf lib && rimraf tsconfig.tsbuildinfo",
     "prepublishOnly": "npm run build",
+    "test": "jest",
+    "test:cov": "jest --collect-coverage",
+    "test:debug": "node --inspect-brk node_modules/.bin/jest --runInBand",
+    "test:debug:watch": "node --inspect-brk node_modules/.bin/jest --runInBand --watch",
     "watch": "tsc -w --listEmittedFiles"
   },
   "dependencies": {
@@ -51,7 +56,11 @@
     "react": "^17.0.1"
   },
   "devDependencies": {
+    "@jupyterlab/testutils": "^3.1.0-alpha.0",
+    "@types/jest": "^26.0.10",
+    "jest": "^26.4.2",
     "rimraf": "~3.0.0",
+    "ts-jest": "^26.3.0",
     "typescript": "~4.1.3"
   },
   "publishConfig": {

+ 1 - 0
packages/documentsearch/src/index.ts

@@ -9,5 +9,6 @@ export * from './interfaces';
 export * from './searchinstance';
 export * from './searchproviderregistry';
 export * from './tokens';
+export * from './providers/genericsearchprovider';
 export * from './providers/codemirrorsearchprovider';
 export * from './providers/notebooksearchprovider';

+ 4 - 4
packages/documentsearch/src/providers/genericsearchprovider.ts

@@ -6,13 +6,13 @@ import { ISearchProvider, ISearchMatch } from '../interfaces';
 import { ISignal, Signal } from '@lumino/signaling';
 import { Widget } from '@lumino/widgets';
 
-const FOUND_CLASSES = ['cm-string', 'cm-overlay', 'cm-searching'];
+export const FOUND_CLASSES = ['cm-string', 'cm-overlay', 'cm-searching'];
 const SELECTED_CLASSES = ['CodeMirror-selectedtext'];
 
 export class GenericSearchProvider implements ISearchProvider<Widget> {
   /**
    * We choose opt out as most node types should be searched (e.g. script).
-   * Even nodes like <data>, could have innerText we care about.
+   * Even nodes like <data>, could have textContent we care about.
    *
    * Note: nodeName is capitalized, so we do the same here
    */
@@ -158,7 +158,7 @@ export class GenericSearchProvider implements ISearchProvider<Widget> {
         // TODO: support tspan for svg when svg support is added
         const spannedNode = document.createElement('span');
         spannedNode.classList.add(...FOUND_CLASSES);
-        spannedNode.innerText = text;
+        spannedNode.textContent = text;
         // Splice the text out before we add it back in with a span
         node!.textContent = `${node!.textContent!.slice(
           0,
@@ -400,7 +400,7 @@ export class GenericSearchProvider implements ISearchProvider<Widget> {
   private _changed = new Signal<this, void>(this);
 }
 
-interface IGenericSearchMatch extends ISearchMatch {
+export interface IGenericSearchMatch extends ISearchMatch {
   readonly originalNode: Node;
   readonly spanElement: HTMLElement;
   /*

+ 107 - 0
packages/documentsearch/test/documentsearchprovider.spec.ts

@@ -0,0 +1,107 @@
+import {
+  GenericSearchProvider,
+  IGenericSearchMatch,
+  FOUND_CLASSES
+} from '@jupyterlab/documentsearch';
+import { Widget } from '@lumino/widgets';
+
+const MATCH_CLASSES = FOUND_CLASSES.join(' ');
+
+describe('documentsearch/genericsearchprovider', () => {
+  describe('GenericSearchProvider', () => {
+    let provider: GenericSearchProvider;
+    let widget: Widget;
+    let match: IGenericSearchMatch;
+
+    beforeEach(() => {
+      provider = new GenericSearchProvider();
+      widget = new Widget();
+    });
+
+    afterEach(async () => {
+      await provider.endSearch();
+      widget.dispose();
+    });
+
+    function getHTMLForMatch(match: IGenericSearchMatch): string | undefined {
+      return match.spanElement?.closest('pre')?.innerHTML;
+    }
+
+    async function queryOne(query: RegExp) {
+      let matches = (await provider.startQuery(
+        query,
+        widget
+      )) as IGenericSearchMatch[];
+      expect(matches).toHaveLength(1);
+      return matches[0];
+    }
+
+    describe('#startQuery()', () => {
+      it('should highlight text fragment nested in a node', async () => {
+        widget.node.innerHTML = '<pre>xyz</pre>';
+        match = await queryOne(/x/);
+        console.log(match.spanElement);
+        expect(getHTMLForMatch(match)).toBe(
+          `<span class="${MATCH_CLASSES}">x</span>yz`
+        );
+
+        match = await queryOne(/y/);
+        expect(getHTMLForMatch(match)).toBe(
+          `x<span class="${MATCH_CLASSES}">y</span>z`
+        );
+
+        match = await queryOne(/z/);
+        expect(getHTMLForMatch(match)).toBe(
+          `xy<span class="${MATCH_CLASSES}">z</span>`
+        );
+      });
+
+      it('should highlight in presence of nested spans adjacent to text nodes', async () => {
+        widget.node.innerHTML = '<pre><span>x</span>yz</pre>';
+        match = await queryOne(/x/);
+        expect(getHTMLForMatch(match)).toBe(
+          `<span><span class="${MATCH_CLASSES}">x</span></span>yz`
+        );
+
+        match = await queryOne(/y/);
+        expect(getHTMLForMatch(match)).toBe(
+          `<span>x</span><span class="${MATCH_CLASSES}">y</span>z`
+        );
+
+        match = await queryOne(/z/);
+        expect(getHTMLForMatch(match)).toBe(
+          `<span>x</span>y<span class="${MATCH_CLASSES}">z</span>`
+        );
+
+        widget.node.innerHTML = '<pre>x<span>y</span>z</pre>';
+        match = await queryOne(/x/);
+        expect(getHTMLForMatch(match)).toBe(
+          `<span class="${MATCH_CLASSES}">x</span><span>y</span>z`
+        );
+
+        match = await queryOne(/y/);
+        expect(getHTMLForMatch(match)).toBe(
+          `x<span><span class="${MATCH_CLASSES}">y</span></span>z`
+        );
+
+        match = await queryOne(/z/);
+        expect(getHTMLForMatch(match)).toBe(
+          `x<span>y</span><span class="${MATCH_CLASSES}">z</span>`
+        );
+      });
+
+      it('should slice out the match correctly in nested nodes', async () => {
+        widget.node.innerHTML = '<pre><span>xy</span>z</pre>';
+        match = await queryOne(/x/);
+        expect(getHTMLForMatch(match)).toBe(
+          `<span><span class="${MATCH_CLASSES}">x</span>y</span>z`
+        );
+
+        match = await queryOne(/y/);
+        expect(getHTMLForMatch(match)).toBe(
+          `<span>x<span class="${MATCH_CLASSES}">y</span></span>z`
+        );
+      });
+    });
+  });
+});

+ 36 - 0
packages/documentsearch/tsconfig.test.json

@@ -0,0 +1,36 @@
+{
+  "extends": "../../tsconfigbase.test",
+  "include": ["src/*", "test/*"],
+  "references": [
+    {
+      "path": "../apputils"
+    },
+    {
+      "path": "../cells"
+    },
+    {
+      "path": "../codeeditor"
+    },
+    {
+      "path": "../codemirror"
+    },
+    {
+      "path": "../fileeditor"
+    },
+    {
+      "path": "../notebook"
+    },
+    {
+      "path": "../translation"
+    },
+    {
+      "path": "../ui-components"
+    },
+    {
+      "path": "."
+    },
+    {
+      "path": "../../testutils"
+    }
+  ]
+}