diff --git a/plugins/plugin-core-support/src/test/core-standalone/text-search.ts b/plugins/plugin-core-support/src/test/core-standalone/text-search.ts index bdd3f8c6517..b8450fc4b00 100644 --- a/plugins/plugin-core-support/src/test/core-standalone/text-search.ts +++ b/plugins/plugin-core-support/src/test/core-standalone/text-search.ts @@ -14,8 +14,6 @@ * limitations under the License. */ -import * as assert from 'assert' - import { Common, CLI, Keys, ReplExpect, Selectors } from '@kui-shell/test' Common.localDescribe('Text search', function(this: Common.ISuite) { @@ -37,189 +35,154 @@ Common.localDescribe('Text search', function(this: Common.ISuite) { .catch(Common.oops(this, true))) it('should open the search bar when cmd+f is pressed', async () => { - await this.app.client.keys([Keys.ctrlOrMeta, 'f']) - await this.app.client.$('#search-bar').then(_ => _.waitForDisplayed()) + await this.app.client.keys([Keys.ctrlOrMeta, 'F']) + await this.app.client.$('#search-bar').then(_ => _.waitForDisplayed({ timeout: CLI.waitTimeout })) await this.app.client.waitUntil( async () => { - return this.app.client.$('#search-input').then(_ => _.isFocused()) + return this.app.client.$('#search-bar input').then(_ => _.isFocused()) }, { timeout: CLI.waitTimeout } ) }) - xit('should not close the search bar if pressing esc outside of search input', async () => { + it('should not close the search bar if pressing esc outside of search bar', async () => { await this.app.client.$(Selectors.CURRENT_PROMPT_BLOCK).then(_ => _.click()) await this.app.client.keys(Keys.ESCAPE) - await this.app.client.$('#search-bar').then(_ => _.waitForDisplayed()) + await this.app.client.$('#search-bar').then(_ => _.waitForDisplayed({ timeout: 3000 })) }) - xit('should focus on search input when search input is pressed', async () => { + it('should focus on search bar when search bar is pressed', async () => { + await this.app.client.$('#search-bar').then(_ => _.click()) await this.app.client.waitUntil( async () => { - await this.app.client.$('#search-input').then(_ => _.click()) - const hasFocus = await this.app.client.$('#search-input').then(_ => _.isFocused()) + const hasFocus = await this.app.client.$('#search-bar input').then(_ => _.isFocused()) return hasFocus }, - { timeout: CLI.waitTimeout } + { timeout: 3000 } ) }) - it('should close the search bar via ctrl+f', async () => { - await this.app.client.keys(['NULL', Keys.ctrlOrMeta, 'f']) - await this.app.client.$('#search-bar').then(_ => _.waitForDisplayed({ timeout: 20000, reverse: true })) - }) + const closeSearchBar = async () => { + await this.app.client.keys([Keys.ctrlOrMeta, 'F']) + await this.app.client.$('#search-bar').then(_ => _.waitForDisplayed({ timeout: 3000, reverse: true })) + } - // re-open, so that we can test the close button - // !!! Notes: some odd chrome or chromedriver bugs: if you click on - // the close button, then chrome/chromedriver/whatever refuses to - // accept any input; both setValue on the INPUT and the ctrlOrMeta+F - // fail - /* it('should open the search bar when cmd+f is pressed', async () => { - await this.app.client.keys([Keys.ctrlOrMeta, 'f']) - await this.app.client.waitForVisible('#search-bar') + it('should close the search bar via ctrl+f', async () => { + closeSearchBar() }) - it('should close the search bar if clicking the close button', async () => { - await new Promise(resolve => setTimeout(resolve, 5000)) - await this.app.client.click('#search-close-button') - await this.app.client.waitForVisible('#search-bar', 2000, true) // reverse: true - await this.app.client.waitUntil(async () => { - const hasFocus = await this.app.client.hasFocus(ui.Selectors.CURRENT_PROMPT) - return hasFocus - }) - }) */ - + /* + #################################################################################### + # THE FOLLOWING ARE ALL MATCHING TESTS. WE TEST IF THE NUMBER OF MATCHES OF A + # PARTICULAR INPUT MATCHES THAT OUTPUTTED ON THE SEARCH BAR + #################################################################################### + */ const type = async (text: string) => { - await this.app.client.execute( - (text: string) => - navigator.clipboard.writeText(text).then(() => { - document.execCommand('paste') - }), - text - ) - - let idx = 0 + // deleting any existing text in search bar input field + await this.app.client.$('#search-bar input').then(_ => _.setValue('')) + // pasting the input text into the search bar input field + await this.app.client.$('#search-bar input').then(_ => _.setValue(text)) + // making sure the word in the input field is the same word we want to search for await this.app.client.waitUntil( async () => { - const actualText = await this.app.client.$('#search-input').then(_ => _.getValue()) - console.error('3T', actualText) - - if (++idx > 5) { - console.error(`still waiting for search result actualText=${actualText} expectedText=${text}`) - } - + const actualText = await this.app.client.$('#search-bar input').then(_ => _.getValue()) return actualText === text }, - { timeout: CLI.waitTimeout } - ) - } - - const waitForSearchFoundText = async (searchFoundText: string) => { - let idx = 0 - await this.app.client.waitUntil( - async () => { - await this.app.client.$('#search-found-text').then(_ => _.waitForExist()) - const txt = await this.app.client.$('#search-found-text').then(_ => _.getText()) - - if (++idx > 5) { - console.error(`still waiting for search result actualText=${txt} expectedText=${searchFoundText}`) - } - - console.error('4a', txt) - return txt === searchFoundText - }, - { timeout: CLI.waitTimeout } + { timeout: 3000 } ) } const findMatch = (typeText: string, searchFoundText: string) => { - it(`should find ${searchFoundText} for ${typeText}`, async () => { + it(`should find ${searchFoundText} matches for ${typeText}`, async () => { try { - console.error('1', typeText) + // opening the search bar await this.app.client.waitUntil( async () => { - await this.app.client.keys(['NULL', Keys.ctrlOrMeta, 'f']) - console.error('1a') - await this.app.client.$('#search-bar').then(_ => _.waitForDisplayed({ timeout: 4000 })) - return true + await this.app.client.keys([Keys.ctrlOrMeta, 'F']) + await this.app.client.$('#search-bar').then(_ => _.waitForDisplayed({ timeout: 3000 })) + return this.app.client.$('#search-bar input').then(_ => _.isFocused()) }, { timeout: CLI.waitTimeout } ) - - console.error('2') - await this.app.client.waitUntil(() => this.app.client.$('#search-input').then(_ => _.isFocused()), { - timeout: CLI.waitTimeout - }) - - console.error('3') + // typing the word to find matches for into the search bar await type(typeText) - - console.error('4', searchFoundText) - await waitForSearchFoundText(searchFoundText) + // finding number of matches + await this.app.client.waitUntil( + async () => { + await this.app.client.$('#search-bar input').then(_ => _.waitForExist({ timeout: 3000 })) + const txt = await this.app.client.$('#search-bar').then(_ => _.getText()) + return txt === searchFoundText + }, + { timeout: 3000 } + ) } catch (err) { await Common.oops(this, true)(err) } }) } - findMatch('grumble', '4 matches') // two executions plus two 'Command not found: grumble' matches, and no tab title match! + // 4 match test: two executions plus two 'Command not found: grumble' matches + findMatch('grumble', '4') - // 1 match test - it('should close the search bar via ctrl+f', () => - this.app.client - .keys(['NULL', Keys.ctrlOrMeta, 'f']) - .then(() => this.app.client.$('#search-bar')) - .then(_ => _.waitForDisplayed({ timeout: 2000, reverse: true })) - .catch(Common.oops(this, true))) - - findMatch('bojangles', '2 matches') // one execution, plus one "Command not found: bojangles" match (not with carbon themes: plus one tab title match) + // 1 match test: one execution plus one 'Command not found: bojangles' match + closeSearchBar() + findMatch('bojangles', '2') // no matches test - it('should close the search bar via ctrl+f', async () => { - return this.app.client - .keys(['NULL', Keys.ctrlOrMeta, 'f']) - .then(() => this.app.client.$('#search-bar')) - .then(_ => _.waitForDisplayed({ timeout: 2000, reverse: true })) - .catch(Common.oops(this, true)) - }) - // re-open, so that we can test entering text and hitting enter - it('should find nothing when searching for waldo', () => - this.app.client - .keys(['NULL', Keys.ctrlOrMeta, 'f']) - .then(() => this.app.client.$('#search-bar')) - .then(_ => _.waitForDisplayed()) - .then(() => - this.app.client.waitUntil(() => this.app.client.$('#search-input').then(_ => _.isFocused()), { - timeout: CLI.waitTimeout - }) - ) - .then(async () => { - console.error('5') - await type(`waldo`) + closeSearchBar() + findMatch('waldo', '0') - console.error('6') - await waitForSearchFoundText('No matches') - }) - .catch(Common.oops(this, true))) + // ############### !!!!!!!!!!!!!!!!!!!! TODO: test entering text and hitting enter !!!!!!!!!!!!!!!!!!!! ############### // paste test; reload first to start with a clean slate in the text search box it('should reload the app', () => Common.refresh(this)) + + // testing paste and making sure nothing else in Kui intercepts the paste it('should paste into the text search box', async () => { - return this.app.client - .keys(['NULL', Keys.ctrlOrMeta, 'f']) - .then(() => this.app.client.$('#search-bar')) - .then(_ => _.waitForDisplayed()) - .then(() => - this.app.client.waitUntil(() => this.app.client.$('#search-input').then(_ => _.isFocused()), { - timeout: CLI.waitTimeout - }) + // open the search bar and focus it + await this.app.client.keys([Keys.ctrlOrMeta, 'F']) + await this.app.client.$('#search-bar').then(_ => _.waitForDisplayed({ timeout: CLI.waitTimeout })) + await this.app.client.waitUntil( + async () => { + return this.app.client.$('#search-bar input').then(_ => _.isFocused()) + }, + { timeout: CLI.waitTimeout } + ) + + // write text using electron + await this.app.electron.clipboard.writeText('grumble') + await this.app.client.execute(() => document.execCommand('paste')) + + // get text from the search bar + const actualText = await this.app.client.$('#search-bar input').then(_ => _.getValue()) + return actualText === 'grumble' + }) + + /* + #################################################################################### + # TESTING THE CLOSE BUTTON + #################################################################################### + */ + it('should close the search bar if clicking the close button', async () => { + await this.app.client.$('#search-bar button').then(_ => _.click()) + await this.app.client.waitUntil(async () => { + // checking that search bar isn't displayed + const displayResults = await this.app.client + .$('#search-bar') + .then(_ => _.waitForDisplayed({ timeout: 3000, reverse: true })) + // open the search bar and focus it + await this.app.client.keys([Keys.ctrlOrMeta, 'F']) + await this.app.client.$('#search-bar').then(_ => _.waitForDisplayed({ timeout: CLI.waitTimeout })) + await this.app.client.waitUntil( + async () => { + return this.app.client.$('#search-bar input').then(_ => _.isFocused()) + }, + { timeout: CLI.waitTimeout } ) - .then(() => this.app.electron.clipboard.writeText('grumble')) - .then(() => this.app.client.execute(() => document.execCommand('paste'))) - .then(() => this.app.client.$('#search-input')) - .then(_ => _.getValue()) - .then(actual => assert.strictEqual(actual, 'grumble')) // paste made it to #search-input? - .catch(Common.oops(this, true)) + // checking that there's no text in the input field after it's been closed + const textInSearchBar = await this.app.client.$('#search-bar input').then(_ => _.getValue()) + return textInSearchBar === '' && displayResults + }) }) }) diff --git a/plugins/plugin-electron-components/src/components/Search.tsx b/plugins/plugin-electron-components/src/components/Search.tsx index 24e55299082..31b86142a8c 100644 --- a/plugins/plugin-electron-components/src/components/Search.tsx +++ b/plugins/plugin-electron-components/src/components/Search.tsx @@ -1,5 +1,5 @@ /* - * Copyright 2020 The Kubernetes Authors + * Copyright 2021 The Kubernetes Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,15 +13,14 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import React from 'react' -import { i18n } from '@kui-shell/core' -import { Event, FoundInPageResult } from 'electron' -import { Icons } from '@kui-shell/plugin-client-common' +import { SearchInput } from '@patternfly/react-core' +// import { i18n } from '@kui-shell/core' +import { FoundInPageResult } from 'electron' import '../../web/scss/components/Search/Search.scss' -const strings = i18n('plugin-client-common', 'search') +// const strings = i18n('plugin-client-common', 'search') type Props = {} @@ -30,7 +29,7 @@ interface State { result: FoundInPageResult } -export default class Search extends React.PureComponent { +export default class Search extends React.Component { // to help with focus private _input: HTMLInputElement @@ -46,6 +45,16 @@ export default class Search extends React.PureComponent { } } + /** stop findInPage, and clear selections in page */ + private async stopFindInPage() { + return import('electron').then(async ({ remote }) => { + // note: with 'clearSelection', the focus of the input is very + // odd; it is focused, but typing text does nothing until some + // global refresh occurs. maybe this is just a bug in electron 6? + await remote.getCurrentWebContents().stopFindInPage('activateSelection') + }) + } + private initEvents() { document.body.addEventListener('keydown', evt => { if ( @@ -53,7 +62,7 @@ export default class Search extends React.PureComponent { evt.code === 'KeyF' && ((evt.ctrlKey && process.platform !== 'darwin') || evt.metaKey) ) { - if (this.state.isActive && !!this._input && document.activeElement !== this._input) { + if (this.state.isActive && document.activeElement !== this._input) { this.doFocus() } else { this.setState(curState => { @@ -69,15 +78,7 @@ export default class Search extends React.PureComponent { }) } - /** stop findInPage, and clear selections in page */ - private async stopFindInPage() { - return import('electron').then(async ({ remote }) => { - // note: with 'clearSelection', the focus of the input is very - // odd; it is focused, but typing text does nothing until some - // global refresh occurs. maybe this is just a bug in electron 6? - await remote.getCurrentWebContents().stopFindInPage('activateSelection') - }) - } + private readonly _onChange = this.onChange.bind(this) private async onChange() { if (this._input) { @@ -86,7 +87,7 @@ export default class Search extends React.PureComponent { this.setState({ result: undefined }) } else { const { remote } = await import('electron') - + // Registering a callback handler remote.getCurrentWebContents().once('found-in-page', async (event: Event, result: FoundInPageResult) => { this.setState(curState => { if (curState.isActive) { @@ -95,24 +96,12 @@ export default class Search extends React.PureComponent { } }) }) - + // this is where we call the electron API to initiate a new find remote.getCurrentWebContents().findInPage(this._input.value) } } } - /** - * This bit of ugliness works around us not using a proper - * webview to encapsulate the element; without this - * encapsulation, chrome does some funky things with - * focus. For example, when there is no text found, the - * input element oddly ... maintains focus but is not - * typeable until a global refresh. Weird. This also has the - * nice side-effect of (albeit with a small visual glitch) - * having no yellow/red highlight text around the text - * inside the input element. - * - */ private hack() { const v = this._input.value this._input.value = '' @@ -120,68 +109,49 @@ export default class Search extends React.PureComponent { this._input.focus() } - private doFocus(input?: HTMLInputElement) { - if (!!input && !this._input) { - this._input = input - } - + private doFocus() { if (this.state.isActive && this._input) { this._input.focus() } } - /** Summarize results of find, e.g. "3 of 3" */ - private matchCount() { - const { result } = this.state - if (result) { - // exclude the text search itself; TODO move the input element to a webview - const N = result.matches - 1 - const text = N === 0 ? strings('noMatches') : N === 1 ? strings('1Match') : strings('nMatches', N) + private onClear = async () => { + await this.stopFindInPage() + if (this._input) { + this._input.value = '' + } + this.setState({ + result: undefined, + isActive: false + }) + } - // re: id: text-search test needs this - return ( - - {text} - - ) + private readonly _onClear = this.onClear.bind(this) + + private readonly _onRef = (c: HTMLInputElement) => { + if (c) { + this._input = c + this.doFocus() } } public render() { if (!this.state.isActive) { - this._input = undefined return } else { - /** - * NOTE: we need the ref input to manage the focus. - * We want the search input to focus when user hits ctrl+f, - * and stay focused when electron finds match. - * With PatternFly’s SearchInput (function component), we can’t access the refs and manage the focus. - * So, we crafted the search input by ourselves. See issue: https://github.com/IBM/kui/issues/4364 - * - */ - - // re: id, text-search test needs this return ( - + ) } } diff --git a/plugins/plugin-electron-components/web/scss/components/Search/Search.scss b/plugins/plugin-electron-components/web/scss/components/Search/Search.scss index 517d8e4bda4..db71c230472 100644 --- a/plugins/plugin-electron-components/web/scss/components/Search/Search.scss +++ b/plugins/plugin-electron-components/web/scss/components/Search/Search.scss @@ -1,16 +1,16 @@ .kui--search { - .kui--search-match-count { - position: absolute; - right: 3em; - } .pf-c-search-input__icon { - svg { - vertical-align: -0.125em; - } + z-index: 1000; + color: var(--color-text-01); } .pf-c-search-input__text-input { - font-family: var(--font-sans-serif); + //font-family: var(--font-sans-serif); + color: var(--color-text-01); + background-color: transparent; + } + + .pf-c-badge.pf-m-read { color: var(--color-text-01); - position: unset; + background-color: var(--color-base02); } }