123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154 |
- import type {ComponentProps} from 'react';
- import {destroyAnnouncer} from '@react-aria/live-announcer';
- import {
- render,
- screen,
- userEvent,
- waitFor,
- within,
- } from 'sentry-test/reactTestingLibrary';
- import {
- SearchQueryBuilder,
- type SearchQueryBuilderProps,
- } from 'sentry/components/searchQueryBuilder';
- import {
- type FieldDefinitionGetter,
- type FilterKeySection,
- QueryInterfaceType,
- } from 'sentry/components/searchQueryBuilder/types';
- import {INTERFACE_TYPE_LOCALSTORAGE_KEY} from 'sentry/components/searchQueryBuilder/utils';
- import {InvalidReason} from 'sentry/components/searchSyntax/parser';
- import type {TagCollection} from 'sentry/types/group';
- import {FieldKey, FieldKind, FieldValueType} from 'sentry/utils/fields';
- import localStorageWrapper from 'sentry/utils/localStorage';
- const FILTER_KEYS: TagCollection = {
- [FieldKey.AGE]: {key: FieldKey.AGE, name: 'Age', kind: FieldKind.FIELD},
- [FieldKey.ASSIGNED]: {
- key: FieldKey.ASSIGNED,
- name: 'Assigned To',
- kind: FieldKind.FIELD,
- predefined: true,
- values: [
- {
- title: 'Suggested',
- type: 'header',
- icon: null,
- children: [{value: 'me'}, {value: 'unassigned'}],
- },
- {
- title: 'All',
- type: 'header',
- icon: null,
- children: [{value: 'person1@sentry.io'}, {value: 'person2@sentry.io'}],
- },
- ],
- },
- [FieldKey.BROWSER_NAME]: {
- key: FieldKey.BROWSER_NAME,
- name: 'Browser Name',
- kind: FieldKind.FIELD,
- predefined: true,
- values: ['Chrome', 'Firefox', 'Safari', 'Edge'],
- },
- [FieldKey.IS]: {
- key: FieldKey.IS,
- name: 'is',
- predefined: true,
- values: ['resolved', 'unresolved', 'ignored'],
- },
- [FieldKey.TIMES_SEEN]: {
- key: FieldKey.TIMES_SEEN,
- name: 'timesSeen',
- kind: FieldKind.FIELD,
- },
- custom_tag_name: {
- key: 'custom_tag_name',
- name: 'Custom_Tag_Name',
- },
- };
- const FITLER_KEY_SECTIONS: FilterKeySection[] = [
- {
- value: FieldKind.FIELD,
- label: 'Category 1',
- children: [
- FieldKey.AGE,
- FieldKey.ASSIGNED,
- FieldKey.BROWSER_NAME,
- FieldKey.IS,
- FieldKey.TIMES_SEEN,
- ],
- },
- {
- value: FieldKind.TAG,
- label: 'Category 2',
- children: ['custom_tag_name'],
- },
- ];
- function getLastInput() {
- const input = screen.getAllByRole('combobox', {name: 'Add a search term'}).at(-1);
- expect(input).toBeInTheDocument();
- return input!;
- }
- describe('SearchQueryBuilder', function () {
- beforeEach(() => {
- // `useDimensions` is used to hide things when the component is too small, so we need to mock a large width
- Object.defineProperty(Element.prototype, 'clientWidth', {value: 1000});
- // Combobox announcements will pollute the test output if we don't clear them
- destroyAnnouncer();
- });
- afterEach(function () {
- jest.restoreAllMocks();
- });
- const defaultProps: ComponentProps<typeof SearchQueryBuilder> = {
- getTagValues: jest.fn(),
- initialQuery: '',
- filterKeySections: FITLER_KEY_SECTIONS,
- filterKeys: FILTER_KEYS,
- label: 'Query Builder',
- searchSource: '',
- };
- it('displays a placeholder when empty', async function () {
- render(<SearchQueryBuilder {...defaultProps} placeholder="foo" />);
- expect(await screen.findByPlaceholderText('foo')).toBeInTheDocument();
- });
- describe('callbacks', function () {
- it('calls onChange, onBlur, and onSearch with the query string', async function () {
- const mockOnChange = jest.fn();
- const mockOnBlur = jest.fn();
- const mockOnSearch = jest.fn();
- render(
- <SearchQueryBuilder
- {...defaultProps}
- initialQuery="a"
- onChange={mockOnChange}
- onBlur={mockOnBlur}
- onSearch={mockOnSearch}
- />
- );
- await userEvent.click(getLastInput());
- await userEvent.keyboard('b{enter}');
- const expectedQueryState = expect.objectContaining({
- parsedQuery: expect.arrayContaining([expect.any(Object)]),
- queryIsValid: true,
- });
- // Should call onChange and onSearch after enter
- await waitFor(() => {
- expect(mockOnChange).toHaveBeenCalledTimes(1);
- expect(mockOnChange).toHaveBeenCalledWith('ab', expectedQueryState);
- expect(mockOnSearch).toHaveBeenCalledTimes(1);
- expect(mockOnSearch).toHaveBeenCalledWith('ab', expectedQueryState);
- });
- await userEvent.click(document.body);
- // Clicking outside activates onBlur
- await waitFor(() => {
- expect(mockOnBlur).toHaveBeenCalledTimes(1);
- expect(mockOnBlur).toHaveBeenCalledWith('ab', expectedQueryState);
- });
- });
- });
- describe('actions', function () {
- it('can clear the query', async function () {
- const mockOnChange = jest.fn();
- const mockOnSearch = jest.fn();
- render(
- <SearchQueryBuilder
- {...defaultProps}
- initialQuery="browser.name:firefox"
- onChange={mockOnChange}
- onSearch={mockOnSearch}
- />
- );
- userEvent.click(screen.getByRole('button', {name: 'Clear search query'}));
- await waitFor(() => {
- expect(mockOnChange).toHaveBeenCalledWith('', expect.anything());
- expect(mockOnSearch).toHaveBeenCalledWith('', expect.anything());
- });
- expect(
- screen.queryByRole('row', {name: 'browser.name:firefox'})
- ).not.toBeInTheDocument();
- expect(screen.getByRole('combobox')).toHaveFocus();
- });
- it('is hidden at small sizes', function () {
- Object.defineProperty(Element.prototype, 'clientWidth', {value: 100});
- const mockOnChange = jest.fn();
- render(
- <SearchQueryBuilder
- {...defaultProps}
- initialQuery="browser.name:firefox"
- onChange={mockOnChange}
- />
- );
- expect(
- screen.queryByRole('button', {name: 'Clear search query'})
- ).not.toBeInTheDocument();
- });
- });
- describe('disabled', function () {
- it('disables all interactable elements', function () {
- const mockOnChange = jest.fn();
- render(
- <SearchQueryBuilder
- {...defaultProps}
- initialQuery="browser.name:firefox"
- onChange={mockOnChange}
- disabled
- />
- );
- expect(getLastInput()).toBeDisabled();
- expect(
- screen.queryByRole('button', {name: 'Clear search query'})
- ).not.toBeInTheDocument();
- expect(
- screen.getByRole('button', {name: 'Remove filter: browser.name'})
- ).toBeDisabled();
- expect(
- screen.getByRole('button', {name: 'Edit operator for filter: browser.name'})
- ).toBeDisabled();
- expect(
- screen.getByRole('button', {name: 'Edit value for filter: browser.name'})
- ).toBeDisabled();
- });
- });
- describe('plain text interface', function () {
- beforeEach(() => {
- localStorageWrapper.setItem(
- INTERFACE_TYPE_LOCALSTORAGE_KEY,
- JSON.stringify(QueryInterfaceType.TEXT)
- );
- });
- it('can change the query by typing', async function () {
- const mockOnChange = jest.fn();
- render(
- <SearchQueryBuilder
- {...defaultProps}
- initialQuery="browser.name:firefox"
- onChange={mockOnChange}
- queryInterface={QueryInterfaceType.TEXT}
- />
- );
- expect(screen.getByRole('textbox')).toHaveValue('browser.name:firefox');
- await userEvent.type(screen.getByRole('textbox'), ' assigned:me');
- expect(screen.getByRole('textbox')).toHaveValue('browser.name:firefox assigned:me');
- await waitFor(() => {
- expect(mockOnChange).toHaveBeenLastCalledWith(
- 'browser.name:firefox assigned:me',
- expect.anything()
- );
- });
- });
- });
- describe('mouse interactions', function () {
- it('can remove a token by clicking the delete button', async function () {
- render(
- <SearchQueryBuilder
- {...defaultProps}
- initialQuery="browser.name:firefox custom_tag_name:123"
- />
- );
- expect(screen.getByRole('row', {name: 'browser.name:firefox'})).toBeInTheDocument();
- expect(screen.getByRole('row', {name: 'custom_tag_name:123'})).toBeInTheDocument();
- await userEvent.click(
- within(screen.getByRole('row', {name: 'browser.name:firefox'})).getByRole(
- 'button',
- {name: 'Remove filter: browser.name'}
- )
- );
- // Browser name token should be removed
- expect(
- screen.queryByRole('row', {name: 'browser.name:firefox'})
- ).not.toBeInTheDocument();
- // Custom tag token should still be present
- expect(screen.getByRole('row', {name: 'custom_tag_name:123'})).toBeInTheDocument();
- });
- it('can modify the operator by clicking into it', async function () {
- render(
- <SearchQueryBuilder {...defaultProps} initialQuery="browser.name:firefox" />
- );
- // Should display as "is" to start
- expect(
- within(
- screen.getByRole('button', {name: 'Edit operator for filter: browser.name'})
- ).getByText('is')
- ).toBeInTheDocument();
- await userEvent.click(
- screen.getByRole('button', {name: 'Edit operator for filter: browser.name'})
- );
- await userEvent.click(screen.getByRole('option', {name: 'browser.name is not'}));
- // Token should be modified to be negated
- expect(
- screen.getByRole('row', {name: '!browser.name:firefox'})
- ).toBeInTheDocument();
- // Should now have "is not" label
- expect(
- within(
- screen.getByRole('button', {name: 'Edit operator for filter: browser.name'})
- ).getByText('is not')
- ).toBeInTheDocument();
- });
- it('escapes values with spaces and reserved characters', async function () {
- render(<SearchQueryBuilder {...defaultProps} initialQuery="" />);
- await userEvent.click(screen.getByRole('combobox', {name: 'Add a search term'}));
- await userEvent.type(
- screen.getByRole('combobox', {name: 'Add a search term'}),
- 'assigned:some" value{enter}'
- );
- // Value should be surrounded by quotes and escaped
- expect(
- screen.getByRole('row', {name: 'assigned:"some\\" value"'})
- ).toBeInTheDocument();
- // Display text should be display the original value
- expect(
- within(
- screen.getByRole('button', {name: 'Edit value for filter: assigned'})
- ).getByText('some" value')
- ).toBeInTheDocument();
- });
- it('can remove parens by clicking the delete button', async function () {
- render(<SearchQueryBuilder {...defaultProps} initialQuery="(" />);
- expect(screen.getByRole('row', {name: '('})).toBeInTheDocument();
- await userEvent.click(screen.getByRole('gridcell', {name: 'Delete ('}));
- expect(screen.queryByRole('row', {name: '('})).not.toBeInTheDocument();
- });
- it('can remove boolean ops by clicking the delete button', async function () {
- render(<SearchQueryBuilder {...defaultProps} initialQuery="OR" />);
- expect(screen.getByRole('row', {name: 'OR'})).toBeInTheDocument();
- await userEvent.click(screen.getByRole('gridcell', {name: 'Delete OR'}));
- expect(screen.queryByRole('row', {name: 'OR'})).not.toBeInTheDocument();
- });
- it('can click and drag to select tokens', async function () {
- render(<SearchQueryBuilder {...defaultProps} initialQuery="is:unresolved" />);
- const grid = screen.getByRole('grid');
- const tokens = screen.getAllByRole('row');
- const freeText1 = tokens[0];
- const filter = tokens[1];
- const freeText2 = tokens[2];
- // jsdom does not support getBoundingClientRect, so we need to mock it for each item
- // First freeText area is 5px wide
- freeText1.getBoundingClientRect = () => {
- return {
- top: 0,
- left: 10,
- bottom: 10,
- right: 15,
- width: 5,
- height: 10,
- } as DOMRect;
- };
- // "is:unresolved" filter is 100px wide
- filter.getBoundingClientRect = () => {
- return {
- top: 0,
- left: 15,
- bottom: 10,
- right: 115,
- width: 100,
- height: 10,
- } as DOMRect;
- };
- // Last freeText area is 200px wide
- freeText2.getBoundingClientRect = () => {
- return {
- top: 0,
- left: 115,
- bottom: 10,
- right: 315,
- width: 200,
- height: 10,
- } as DOMRect;
- };
- // Note that jsdom does not do layout, so all coordinates are 0, 0
- await userEvent.pointer([
- // Start with 0, 5 so that we are on the first token
- {keys: '[MouseLeft>]', target: grid, coords: {x: 0, y: 5}},
- // Move to 50, 5 (within filter token)
- {target: grid, coords: {x: 50, y: 5}},
- ]);
- // all should be selected except the last free text
- await waitFor(() => {
- expect(freeText1).toHaveAttribute('aria-selected', 'true');
- });
- expect(filter).toHaveAttribute('aria-selected', 'true');
- expect(freeText2).toHaveAttribute('aria-selected', 'false');
- // Now move pointer to the end and below to select everything
- await userEvent.pointer([{target: grid, coords: {x: 400, y: 50}}]);
- // All tokens should be selected
- await waitFor(() => {
- expect(freeText2).toHaveAttribute('aria-selected', 'true');
- });
- expect(freeText1).toHaveAttribute('aria-selected', 'true');
- expect(filter).toHaveAttribute('aria-selected', 'true');
- // Now move pointer back to original position
- await userEvent.pointer([
- // Move to 100, 1 to select all tokens (which are at 0, 0)
- {target: grid, coords: {x: 0, y: 5}},
- // Release mouse button to finish selection
- {keys: '[/MouseLeft]', target: getLastInput()},
- ]);
- // All tokens should be deselected
- await waitFor(() => {
- expect(freeText1).toHaveAttribute('aria-selected', 'false');
- });
- expect(filter).toHaveAttribute('aria-selected', 'false');
- expect(freeText2).toHaveAttribute('aria-selected', 'false');
- });
- });
- describe('new search tokens', function () {
- it('can add an unsupported filter key and value', async function () {
- render(<SearchQueryBuilder {...defaultProps} />);
- await userEvent.click(getLastInput());
- // Typing "foo", then " a:b" should add the "foo" text followed by a new token "a:b"
- await userEvent.type(
- screen.getByRole('combobox', {name: 'Add a search term'}),
- 'foo a:b{enter}'
- );
- expect(screen.getByRole('row', {name: 'foo'})).toBeInTheDocument();
- expect(screen.getByRole('row', {name: 'a:b'})).toBeInTheDocument();
- });
- it('adds default value for filter when typing <filter>:', async function () {
- render(<SearchQueryBuilder {...defaultProps} />);
- await userEvent.click(getLastInput());
- // Typing `is:` and escaping should result in `is:unresolved`
- await userEvent.type(
- screen.getByRole('combobox', {name: 'Add a search term'}),
- 'is:{escape}'
- );
- expect(await screen.findByRole('row', {name: 'is:unresolved'})).toBeInTheDocument();
- });
- it('does not automatically create a filter if the user intends to wrap in quotes', async function () {
- render(<SearchQueryBuilder {...defaultProps} />);
- await userEvent.click(getLastInput());
- // Starting with an opening quote and typing out Error: should stay as raw text
- await userEvent.type(
- screen.getByRole('combobox', {name: 'Add a search term'}),
- '"Error: foo"'
- );
- await waitFor(() => {
- expect(getLastInput()).toHaveValue('"Error: foo"');
- });
- });
- it('breaks keys into sections', async function () {
- render(<SearchQueryBuilder {...defaultProps} />);
- await userEvent.click(screen.getByRole('combobox', {name: 'Add a search term'}));
- const menu = screen.getByRole('listbox');
- const groups = within(menu).getAllByRole('group');
- expect(groups).toHaveLength(2);
- // First group (Field) should have age, assigned, browser.name
- const group1 = groups[0];
- expect(within(group1).getByRole('option', {name: 'age'})).toBeInTheDocument();
- expect(within(group1).getByRole('option', {name: 'assigned'})).toBeInTheDocument();
- expect(
- within(group1).getByRole('option', {name: 'browser.name'})
- ).toBeInTheDocument();
- // Second group (Tag) should have custom_tag_name
- const group2 = groups[1];
- expect(
- within(group2).getByRole('option', {name: 'custom_tag_name'})
- ).toBeInTheDocument();
- });
- it('can search by key description', async function () {
- render(<SearchQueryBuilder {...defaultProps} />);
- await userEvent.click(screen.getByRole('combobox', {name: 'Add a search term'}));
- await userEvent.keyboard('assignee');
- // "assignee" is in the description of "assigned"
- expect(await screen.findByRole('option', {name: 'assigned'})).toBeInTheDocument();
- });
- it('can add a new token by clicking a key suggestion', async function () {
- render(<SearchQueryBuilder {...defaultProps} />);
- await userEvent.click(screen.getByRole('combobox', {name: 'Add a search term'}));
- await userEvent.click(screen.getByRole('option', {name: 'browser.name'}));
- // New token should be added with the correct key and default value
- expect(screen.getByRole('row', {name: 'browser.name:""'})).toBeInTheDocument();
- await userEvent.click(screen.getByRole('option', {name: 'Firefox'}));
- // New token should have a value
- expect(screen.getByRole('row', {name: 'browser.name:Firefox'})).toBeInTheDocument();
- });
- it('can add free text by typing', async function () {
- const mockOnSearch = jest.fn();
- render(<SearchQueryBuilder {...defaultProps} onSearch={mockOnSearch} />);
- await userEvent.click(getLastInput());
- await userEvent.type(screen.getByRole('combobox'), 'some free text{enter}');
- await waitFor(() => {
- expect(mockOnSearch).toHaveBeenCalledWith('some free text', expect.anything());
- });
- // Should still have text in the input
- expect(screen.getByRole('combobox')).toHaveValue('some free text');
- // Should have closed the menu
- expect(screen.getByRole('combobox')).toHaveAttribute('aria-expanded', 'false');
- });
- it('can add a filter after some free text', async function () {
- render(<SearchQueryBuilder {...defaultProps} />);
- await userEvent.click(getLastInput());
- // XXX(malwilley): SearchQueryBuilderInput updates state in the render
- // function which causes an act warning despite using userEvent.click.
- // Cannot find a way to avoid this warning.
- jest.spyOn(console, 'error').mockImplementation(jest.fn());
- await userEvent.type(
- screen.getByRole('combobox'),
- 'some free text brow{ArrowDown}{Enter}'
- );
- jest.restoreAllMocks();
- // Filter value should have focus
- expect(screen.getByRole('combobox', {name: 'Edit filter value'})).toHaveFocus();
- await userEvent.keyboard('foo{enter}');
- // Should have a free text token "some free text"
- expect(
- await screen.findByRole('row', {name: /some free text/})
- ).toBeInTheDocument();
- // Should have a filter token "browser.name:foo"
- expect(screen.getByRole('row', {name: 'browser.name:foo'})).toBeInTheDocument();
- });
- it('can add parens by typing', async function () {
- render(<SearchQueryBuilder {...defaultProps} />);
- await userEvent.click(getLastInput());
- await userEvent.keyboard('(');
- expect(await screen.findByRole('row', {name: '('})).toBeInTheDocument();
- expect(getLastInput()).toHaveFocus();
- });
- });
- describe('keyboard interactions', function () {
- beforeEach(() => {
- // jsdom does not support clipboard API
- Object.assign(navigator, {
- clipboard: {
- writeText: jest.fn().mockResolvedValue(''),
- },
- });
- });
- it('can remove a previous token by pressing backspace', async function () {
- render(
- <SearchQueryBuilder {...defaultProps} initialQuery="browser.name:firefox" />
- );
- // Focus into search (cursor be at end of the query)
- await userEvent.click(getLastInput());
- // Pressing backspace once should focus the previous token
- await userEvent.keyboard('{backspace}');
- expect(screen.queryByRole('row', {name: 'browser.name:firefox'})).toHaveFocus();
- // Pressing backspace again should remove the token
- await userEvent.keyboard('{backspace}');
- expect(
- screen.queryByRole('row', {name: 'browser.name:firefox'})
- ).not.toBeInTheDocument();
- });
- it('can remove a subsequent token by pressing delete', async function () {
- render(
- <SearchQueryBuilder {...defaultProps} initialQuery="browser.name:firefox" />
- );
- // Put focus into the first input (before the token)
- await userEvent.click(
- screen.getAllByRole('combobox', {name: 'Add a search term'})[0]
- );
- // Pressing delete once should focus the previous token
- await userEvent.keyboard('{delete}');
- expect(screen.queryByRole('row', {name: 'browser.name:firefox'})).toHaveFocus();
- // Pressing delete again should remove the token
- await userEvent.keyboard('{delete}');
- expect(
- screen.queryByRole('row', {name: 'browser.name:firefox'})
- ).not.toBeInTheDocument();
- });
- it('can navigate between tokens with arrow keys', async function () {
- render(
- <SearchQueryBuilder
- {...defaultProps}
- initialQuery="browser.name:firefox abc assigned:me"
- />
- );
- await userEvent.click(getLastInput());
- // Focus should be in the last text input
- expect(
- screen.getAllByRole('combobox', {name: 'Add a search term'}).at(-1)
- ).toHaveFocus();
- // Left once focuses the assigned remove button
- await userEvent.keyboard('{arrowleft}');
- expect(screen.getByRole('button', {name: 'Remove filter: assigned'})).toHaveFocus();
- // Left again focuses the assigned filter value
- await userEvent.keyboard('{arrowleft}');
- expect(
- screen.getByRole('button', {name: 'Edit value for filter: assigned'})
- ).toHaveFocus();
- // Left again focuses the assigned operator
- await userEvent.keyboard('{arrowleft}');
- expect(
- screen.getByRole('button', {name: 'Edit operator for filter: assigned'})
- ).toHaveFocus();
- // Left again goes to the next text input between tokens
- await userEvent.keyboard('{arrowleft}');
- expect(
- screen.getAllByRole('combobox', {name: 'Add a search term'}).at(-2)
- ).toHaveFocus();
- // 4 more lefts go through the input text "abc" and to the next token
- await userEvent.keyboard('{arrowleft}{arrowleft}{arrowleft}{arrowleft}');
- expect(
- screen.getByRole('button', {name: 'Remove filter: browser.name'})
- ).toHaveFocus();
- // 1 right goes back to the text input
- await userEvent.keyboard('{arrowright}');
- expect(
- screen.getAllByRole('combobox', {name: 'Add a search term'}).at(-2)
- ).toHaveFocus();
- });
- it('when focus is in a filter segment, backspace first focuses the filter then deletes it', async function () {
- render(
- <SearchQueryBuilder {...defaultProps} initialQuery="browser.name:firefox" />
- );
- // Focus into search (cursor be at end of the query)
- screen
- .getByRole('button', {name: 'Edit operator for filter: browser.name'})
- .focus();
- // Pressing backspace once should focus the token
- await userEvent.keyboard('{backspace}');
- expect(screen.queryByRole('row', {name: 'browser.name:firefox'})).toHaveFocus();
- // Pressing backspace again should remove the token
- await userEvent.keyboard('{backspace}');
- expect(
- screen.queryByRole('row', {name: 'browser.name:firefox'})
- ).not.toBeInTheDocument();
- });
- it('has a single tab stop', async function () {
- render(
- <SearchQueryBuilder {...defaultProps} initialQuery="browser.name:firefox" />
- );
- expect(document.body).toHaveFocus();
- // Tabbing in should focus the last input
- await userEvent.keyboard('{Tab}');
- expect(
- screen.getAllByRole('combobox', {name: 'Add a search term'}).at(-1)
- ).toHaveFocus();
- // One more tab should go to the clear button
- await userEvent.keyboard('{Tab}');
- expect(screen.getByRole('button', {name: 'Clear search query'})).toHaveFocus();
- // Another should exit component
- await userEvent.keyboard('{Tab}');
- expect(document.body).toHaveFocus();
- });
- it('converts pasted text into tokens', async function () {
- render(<SearchQueryBuilder {...defaultProps} initialQuery="" />);
- await userEvent.click(getLastInput());
- await userEvent.paste('browser.name:firefox');
- // Should have tokenized the pasted text
- expect(screen.getByRole('row', {name: 'browser.name:firefox'})).toBeInTheDocument();
- // Focus should be at the end of the pasted text
- expect(
- screen.getAllByRole('combobox', {name: 'Add a search term'}).at(-1)
- ).toHaveFocus();
- });
- it('can remove parens with the keyboard', async function () {
- render(<SearchQueryBuilder {...defaultProps} initialQuery="(" />);
- expect(screen.getByRole('row', {name: '('})).toBeInTheDocument();
- await userEvent.click(getLastInput());
- await userEvent.keyboard('{backspace}{backspace}');
- expect(screen.queryByRole('row', {name: '('})).not.toBeInTheDocument();
- });
- it('can remove boolean ops with the keyboard', async function () {
- render(<SearchQueryBuilder {...defaultProps} initialQuery="and" />);
- expect(screen.getByRole('row', {name: 'and'})).toBeInTheDocument();
- await userEvent.click(getLastInput());
- await userEvent.keyboard('{backspace}{backspace}');
- expect(screen.queryByRole('row', {name: 'and'})).not.toBeInTheDocument();
- });
- it('exits filter value when pressing escape', async function () {
- render(
- <SearchQueryBuilder {...defaultProps} initialQuery="browser.name:Firefox" />
- );
- // Click into filter value (button to edit will no longer exist)
- await userEvent.click(
- screen.getByRole('button', {name: 'Edit value for filter: browser.name'})
- );
- expect(
- screen.queryByRole('button', {name: 'Edit value for filter: browser.name'})
- ).not.toBeInTheDocument();
- // Pressing escape will exit the filter value, so edit button will come back
- await userEvent.keyboard('{Escape}');
- expect(
- await screen.findByRole('button', {name: 'Edit value for filter: browser.name'})
- ).toBeInTheDocument();
- // Focus should now be to the right of the filter
- expect(
- screen.getAllByRole('combobox', {name: 'Add a search term'}).at(-1)
- ).toHaveFocus();
- });
- it('backspace focuses filter when input is empty', async function () {
- const mockOnChange = jest.fn();
- render(
- <SearchQueryBuilder
- {...defaultProps}
- onChange={mockOnChange}
- initialQuery="age:-24h"
- />
- );
- // Click into filter value (button to edit will no longer exist)
- await userEvent.click(
- screen.getByRole('button', {name: 'Edit value for filter: age'})
- );
- await userEvent.keyboard('{Backspace}');
- // Filter should now have focus, and no changes should have been made
- expect(screen.getByRole('row', {name: 'age:-24h'})).toHaveFocus();
- expect(mockOnChange).not.toHaveBeenCalled();
- });
- it('can select all and delete with ctrl+a', async function () {
- const mockOnChange = jest.fn();
- render(
- <SearchQueryBuilder
- {...defaultProps}
- onChange={mockOnChange}
- initialQuery="browser.name:firefox foo"
- />
- );
- await userEvent.click(getLastInput());
- await userEvent.keyboard('{Control>}a{/Control}');
- // Should have selected the entire query
- for (const token of screen.getAllByRole('row')) {
- expect(token).toHaveAttribute('aria-selected', 'true');
- }
- // Focus should be on the selection key handler input
- expect(screen.getByTestId('selection-key-handler')).toHaveFocus();
- // Pressing delete should remove all selected tokens
- await userEvent.keyboard('{Backspace}');
- expect(mockOnChange).toHaveBeenCalledWith('', expect.anything());
- });
- it('focus goes to first input after ctrl+a and arrow left', async function () {
- render(
- <SearchQueryBuilder {...defaultProps} initialQuery="browser.name:firefox" />
- );
- await userEvent.click(getLastInput());
- await userEvent.keyboard('{Control>}a{/Control}');
- // Pressing arrow left should put focus in first text input
- await userEvent.keyboard('{ArrowLeft}');
- expect(
- screen.getAllByRole('combobox', {name: 'Add a search term'}).at(0)
- ).toHaveFocus();
- });
- it('focus goes to last input after ctrl+a and arrow right', async function () {
- render(
- <SearchQueryBuilder {...defaultProps} initialQuery="browser.name:firefox" />
- );
- await userEvent.click(getLastInput());
- await userEvent.keyboard('{Control>}a{/Control}');
- // Pressing arrow right should put focus in last text input
- await userEvent.keyboard('{ArrowRight}');
- expect(
- screen.getAllByRole('combobox', {name: 'Add a search term'}).at(-1)
- ).toHaveFocus();
- });
- it('replaces selection when a key is pressed', async function () {
- const mockOnChange = jest.fn();
- render(
- <SearchQueryBuilder
- {...defaultProps}
- initialQuery="browser.name:firefox"
- onChange={mockOnChange}
- />
- );
- await userEvent.click(getLastInput());
- await userEvent.keyboard('{Control>}a{/Control}');
- await userEvent.keyboard('foo');
- expect(
- screen.queryByRole('row', {name: 'browser.name:firefox'})
- ).not.toBeInTheDocument();
- expect(getLastInput()).toHaveFocus();
- expect(getLastInput()).toHaveValue('foo');
- });
- it('replaces selection with pasted content with ctrl+v', async function () {
- const mockOnChange = jest.fn();
- render(
- <SearchQueryBuilder
- {...defaultProps}
- initialQuery="browser.name:firefox"
- onChange={mockOnChange}
- />
- );
- await userEvent.click(getLastInput());
- await userEvent.keyboard('{Control>}a{/Control}');
- await userEvent.paste('foo');
- expect(
- screen.queryByRole('row', {name: 'browser.name:firefox'})
- ).not.toBeInTheDocument();
- expect(getLastInput()).toHaveFocus();
- expect(getLastInput()).toHaveValue('foo');
- });
- it('can copy selection with ctrl-c', async function () {
- render(
- <SearchQueryBuilder {...defaultProps} initialQuery="browser.name:firefox foo" />
- );
- await userEvent.click(getLastInput());
- await userEvent.keyboard('{Control>}a{/Control}');
- await userEvent.keyboard('{Control>}c{/Control}');
- expect(navigator.clipboard.writeText).toHaveBeenCalledWith(
- 'browser.name:firefox foo'
- );
- });
- it('can cut selection with ctrl-x', async function () {
- const mockOnChange = jest.fn();
- render(
- <SearchQueryBuilder
- {...defaultProps}
- initialQuery="browser.name:firefox"
- onChange={mockOnChange}
- />
- );
- await userEvent.click(getLastInput());
- await userEvent.keyboard('{Control>}a{/Control}');
- await userEvent.keyboard('{Control>}x{/Control}');
- expect(navigator.clipboard.writeText).toHaveBeenCalledWith('browser.name:firefox');
- expect(mockOnChange).toHaveBeenCalledWith('', expect.anything());
- });
- it('can undo last action with ctrl-z', async function () {
- render(
- <SearchQueryBuilder {...defaultProps} initialQuery="browser.name:firefox" />
- );
- // Clear search query removes the token
- await userEvent.click(screen.getByRole('button', {name: 'Clear search query'}));
- expect(
- screen.queryByRole('row', {name: 'browser.name:firefox'})
- ).not.toBeInTheDocument();
- // Ctrl+Z adds it back
- await userEvent.keyboard('{Control>}z{/Control}');
- expect(
- await screen.findByRole('row', {name: 'browser.name:firefox'})
- ).toBeInTheDocument();
- });
- it('works with excess undo actions', async function () {
- render(
- <SearchQueryBuilder {...defaultProps} initialQuery="browser.name:firefox" />
- );
- // Remove the token
- await userEvent.click(
- screen.getByRole('button', {name: 'Remove filter: browser.name'})
- );
- await waitFor(() => {
- expect(
- screen.queryByRole('row', {name: 'browser.name:firefox'})
- ).not.toBeInTheDocument();
- });
- // Ctrl+Z adds it back
- await userEvent.keyboard('{Control>}z{/Control}');
- expect(
- await screen.findByRole('row', {name: 'browser.name:firefox'})
- ).toBeInTheDocument();
- // Extra Ctrl-Z should not do anything
- await userEvent.keyboard('{Control>}z{/Control}');
- // Remove token again
- await userEvent.click(
- screen.getByRole('button', {name: 'Remove filter: browser.name'})
- );
- await waitFor(() => {
- expect(
- screen.queryByRole('row', {name: 'browser.name:firefox'})
- ).not.toBeInTheDocument();
- });
- // Ctrl+Z adds it back again
- await userEvent.keyboard('{Control>}z{/Control}');
- expect(
- await screen.findByRole('row', {name: 'browser.name:firefox'})
- ).toBeInTheDocument();
- });
- });
- describe('token values', function () {
- it('supports grouped token value suggestions', async function () {
- render(<SearchQueryBuilder {...defaultProps} initialQuery="assigned:me" />);
- await userEvent.click(
- screen.getByRole('button', {name: 'Edit value for filter: assigned'})
- );
- const groups = within(screen.getByRole('listbox')).getAllByRole('group');
- // First group is selected option, second is "Suggested", third is "All"
- expect(groups).toHaveLength(3);
- expect(
- within(screen.getByRole('listbox')).getByText('Suggested')
- ).toBeInTheDocument();
- expect(within(screen.getByRole('listbox')).getByText('All')).toBeInTheDocument();
- // First group is the selected "me"
- expect(within(groups[0]).getByRole('option', {name: 'me'})).toBeInTheDocument();
- // Second group is the remaining option in the "Suggested" section
- expect(
- within(groups[1]).getByRole('option', {name: 'unassigned'})
- ).toBeInTheDocument();
- // Third group are the options under the "All" section
- expect(
- within(groups[2]).getByRole('option', {name: 'person1@sentry.io'})
- ).toBeInTheDocument();
- expect(
- within(groups[2]).getByRole('option', {name: 'person2@sentry.io'})
- ).toBeInTheDocument();
- });
- it('fetches tag values', async function () {
- const mockGetTagValues = jest.fn().mockResolvedValue(['tag_value_one']);
- render(
- <SearchQueryBuilder
- {...defaultProps}
- initialQuery="custom_tag_name:"
- getTagValues={mockGetTagValues}
- />
- );
- await userEvent.click(
- screen.getByRole('button', {name: 'Edit value for filter: custom_tag_name'})
- );
- await screen.findByRole('option', {name: 'tag_value_one'});
- await userEvent.click(screen.getByRole('option', {name: 'tag_value_one'}));
- expect(
- await screen.findByRole('row', {name: 'custom_tag_name:tag_value_one'})
- ).toBeInTheDocument();
- });
- });
- describe('filter types', function () {
- describe('is', function () {
- it('can modify the value by clicking into it', async function () {
- // `is` only accepts single values
- render(<SearchQueryBuilder {...defaultProps} initialQuery="is:unresolved" />);
- // Should display as "unresolved" to start
- expect(
- within(
- screen.getByRole('button', {name: 'Edit value for filter: is'})
- ).getByText('unresolved')
- ).toBeInTheDocument();
- await userEvent.click(
- screen.getByRole('button', {name: 'Edit value for filter: is'})
- );
- // Should have placeholder text of previous value
- expect(screen.getByRole('combobox', {name: 'Edit filter value'})).toHaveAttribute(
- 'placeholder',
- 'unresolved'
- );
- // Clicking the "resolved" option should update the value
- await userEvent.click(await screen.findByRole('option', {name: 'resolved'}));
- expect(screen.getByRole('row', {name: 'is:resolved'})).toBeInTheDocument();
- expect(
- within(
- screen.getByRole('button', {name: 'Edit value for filter: is'})
- ).getByText('resolved')
- ).toBeInTheDocument();
- });
- it('defaults to unresolved when there is no value', async function () {
- render(<SearchQueryBuilder {...defaultProps} initialQuery="is:" />);
- // Click into value and press enter with no value
- await userEvent.click(
- screen.getByRole('button', {name: 'Edit value for filter: is'})
- );
- await userEvent.keyboard('{enter}');
- // Should be is:unresolved
- expect(
- await screen.findByRole('row', {name: 'is:unresolved'})
- ).toBeInTheDocument();
- });
- });
- describe('has', function () {
- it('display has and does not have as options', async function () {
- const mockOnChange = jest.fn();
- render(
- <SearchQueryBuilder
- {...defaultProps}
- onChange={mockOnChange}
- initialQuery="has:key"
- />
- );
- expect(
- within(
- screen.getByRole('button', {name: 'Edit value for filter: has'})
- ).getByText('key')
- ).toBeInTheDocument();
- await userEvent.click(
- screen.getByRole('button', {name: 'Edit operator for filter: has'})
- );
- await userEvent.click(await screen.findByRole('option', {name: 'does not have'}));
- await waitFor(() => {
- expect(mockOnChange).toHaveBeenCalledWith('!has:key', expect.anything());
- });
- expect(
- within(
- screen.getByRole('button', {name: 'Edit operator for filter: has'})
- ).getByText('does not have')
- ).toBeInTheDocument();
- });
- });
- describe('string', function () {
- it('defaults to an empty string when no value is provided', async function () {
- render(
- <SearchQueryBuilder {...defaultProps} initialQuery="browser.name:firefox" />
- );
- await userEvent.click(
- screen.getByRole('button', {name: 'Edit value for filter: browser.name'})
- );
- await userEvent.clear(
- await screen.findByRole('combobox', {name: 'Edit filter value'})
- );
- await userEvent.keyboard('{enter}');
- // Should have empty quotes `""`
- expect(
- await screen.findByRole('row', {name: 'browser.name:""'})
- ).toBeInTheDocument();
- expect(
- within(
- screen.getByRole('button', {name: 'Edit value for filter: browser.name'})
- ).getByText('""')
- ).toBeInTheDocument();
- });
- it('can modify operator for filter with multiple values', async function () {
- render(
- <SearchQueryBuilder
- {...defaultProps}
- initialQuery="browser.name:[firefox,chrome]"
- />
- );
- // Should display as "is" to start
- expect(
- within(
- screen.getByRole('button', {name: 'Edit operator for filter: browser.name'})
- ).getByText('is')
- ).toBeInTheDocument();
- await userEvent.click(
- screen.getByRole('button', {name: 'Edit operator for filter: browser.name'})
- );
- await userEvent.click(screen.getByRole('option', {name: 'browser.name is not'}));
- // Token should be modified to be negated
- expect(
- screen.getByRole('row', {name: '!browser.name:[firefox,chrome]'})
- ).toBeInTheDocument();
- // Should now have "is not" label
- expect(
- within(
- screen.getByRole('button', {name: 'Edit operator for filter: browser.name'})
- ).getByText('is not')
- ).toBeInTheDocument();
- });
- it('can modify the value by clicking into it (multi-select)', async function () {
- render(
- <SearchQueryBuilder {...defaultProps} initialQuery="browser.name:firefox" />
- );
- // Should display as "firefox" to start
- expect(
- within(
- screen.getByRole('button', {name: 'Edit value for filter: browser.name'})
- ).getByText('firefox')
- ).toBeInTheDocument();
- await userEvent.click(
- screen.getByRole('button', {name: 'Edit value for filter: browser.name'})
- );
- // Should start with previous values and an appended ',' for the next value
- await waitFor(() => {
- expect(screen.getByRole('combobox', {name: 'Edit filter value'})).toHaveValue(
- 'firefox,'
- );
- });
- // Clicking the "Chrome option should add it to the list and commit changes
- await userEvent.click(screen.getByRole('option', {name: 'Chrome'}));
- expect(
- screen.getByRole('row', {name: 'browser.name:[firefox,Chrome]'})
- ).toBeInTheDocument();
- const valueButton = screen.getByRole('button', {
- name: 'Edit value for filter: browser.name',
- });
- expect(within(valueButton).getByText('firefox')).toBeInTheDocument();
- expect(within(valueButton).getByText('or')).toBeInTheDocument();
- expect(within(valueButton).getByText('Chrome')).toBeInTheDocument();
- });
- it('keeps focus inside value when multi-selecting with checkboxes', async function () {
- render(
- <SearchQueryBuilder {...defaultProps} initialQuery="browser.name:firefox" />
- );
- await userEvent.click(
- screen.getByRole('button', {name: 'Edit value for filter: browser.name'})
- );
- // Input value should start with previous value and appended ','
- await waitFor(() => {
- expect(screen.getByRole('combobox', {name: 'Edit filter value'})).toHaveValue(
- 'firefox,'
- );
- });
- // Toggling off the "firefox" option should:
- // - Commit an empty string as the filter value
- // - Input value should be cleared
- // - Keep focus inside the input
- await userEvent.click(
- await screen.findByRole('checkbox', {name: 'Toggle firefox'})
- );
- expect(
- await screen.findByRole('row', {name: 'browser.name:""'})
- ).toBeInTheDocument();
- await waitFor(() => {
- expect(screen.getByRole('combobox', {name: 'Edit filter value'})).toHaveValue(
- ''
- );
- });
- expect(screen.getByRole('combobox', {name: 'Edit filter value'})).toHaveFocus();
- // Toggling on the "Chrome" option should:
- // - Commit the value "Chrome" to the filter
- // - Input value should be "Chrome,"
- // - Keep focus inside the input
- await userEvent.click(
- await screen.findByRole('checkbox', {name: 'Toggle Chrome'})
- );
- expect(
- await screen.findByRole('row', {name: 'browser.name:Chrome'})
- ).toBeInTheDocument();
- await waitFor(() => {
- expect(screen.getByRole('combobox', {name: 'Edit filter value'})).toHaveValue(
- 'Chrome,'
- );
- });
- expect(screen.getByRole('combobox', {name: 'Edit filter value'})).toHaveFocus();
- });
- it('collapses many selected options', function () {
- render(
- <SearchQueryBuilder
- {...defaultProps}
- initialQuery="browser.name:[one,two,three,four]"
- />
- );
- const valueButton = screen.getByRole('button', {
- name: 'Edit value for filter: browser.name',
- });
- expect(within(valueButton).getByText('one')).toBeInTheDocument();
- expect(within(valueButton).getByText('two')).toBeInTheDocument();
- expect(within(valueButton).getByText('three')).toBeInTheDocument();
- expect(within(valueButton).getByText('+1')).toBeInTheDocument();
- expect(within(valueButton).queryByText('four')).not.toBeInTheDocument();
- expect(within(valueButton).getAllByText('or')).toHaveLength(2);
- });
- it.each([
- ['spaces', 'a b', '"a b"'],
- ['quotes', 'a"b', '"a\\"b"'],
- ['parens', 'foo()', '"foo()"'],
- ])('tag values escape %s', async (_, value, expected) => {
- const mockOnChange = jest.fn();
- render(
- <SearchQueryBuilder
- {...defaultProps}
- onChange={mockOnChange}
- initialQuery="browser.name:"
- />
- );
- await userEvent.click(
- screen.getByRole('button', {name: 'Edit value for filter: browser.name'})
- );
- await userEvent.keyboard(`${value}{enter}`);
- // Value should be surrounded by quotes and escaped
- await waitFor(() => {
- expect(mockOnChange).toHaveBeenCalledWith(
- `browser.name:${expected}`,
- expect.anything()
- );
- });
- });
- it('can replace a value with a new one', async function () {
- render(
- <SearchQueryBuilder {...defaultProps} initialQuery="browser.name:[1,c,3]" />
- );
- await userEvent.click(
- screen.getByRole('button', {name: 'Edit value for filter: browser.name'})
- );
- await waitFor(() => {
- expect(screen.getByRole('combobox', {name: 'Edit filter value'})).toHaveValue(
- '1,c,3,'
- );
- });
- // Arrow left three times to put cursor inside "c" value
- await userEvent.keyboard('{ArrowLeft}{ArrowLeft}{ArrowLeft}');
- // When on c value, should show options matching "c"
- const chromeOption = await screen.findByRole('option', {name: 'Chrome'});
- // Clicking the "Chrome option should replace "c" with "Chrome" and commit chagnes
- await userEvent.click(chromeOption);
- expect(
- await screen.findByRole('row', {name: 'browser.name:[1,Chrome,3]'})
- ).toBeInTheDocument();
- });
- it('can enter a custom value', async function () {
- render(<SearchQueryBuilder {...defaultProps} initialQuery="browser.name:" />);
- await userEvent.click(
- screen.getByRole('button', {name: 'Edit value for filter: browser.name'})
- );
- await userEvent.keyboard('foo,bar{enter}');
- expect(
- await screen.findByRole('row', {name: 'browser.name:[foo,bar]'})
- ).toBeInTheDocument();
- });
- it('displays comparison operator values with allowAllOperators: true', async function () {
- const filterKeys = {
- [FieldKey.RELEASE_VERSION]: {
- key: FieldKey.RELEASE_VERSION,
- name: '',
- allowAllOperators: true,
- },
- };
- render(
- <SearchQueryBuilder
- {...defaultProps}
- filterKeys={filterKeys}
- filterKeySections={[]}
- initialQuery="release.version:1.0"
- />
- );
- await userEvent.click(
- screen.getByRole('button', {name: 'Edit operator for filter: release.version'})
- );
- // Normally text filters only have 'is' and 'is not' as options
- expect(
- await screen.findByRole('option', {name: 'release.version >'})
- ).toBeInTheDocument();
- await userEvent.click(screen.getByRole('option', {name: 'release.version >'}));
- expect(
- await screen.findByRole('row', {name: 'release.version:>1.0'})
- ).toBeInTheDocument();
- });
- });
- describe('numeric', function () {
- it('new numeric filters start with a value', async function () {
- render(<SearchQueryBuilder {...defaultProps} />);
- await userEvent.click(getLastInput());
- await userEvent.keyboard('time{ArrowDown}{Enter}');
- // Should start with the > operator and a value of 100
- expect(
- await screen.findByRole('row', {name: 'timesSeen:>100'})
- ).toBeInTheDocument();
- });
- it('keeps previous value when confirming empty value', async function () {
- const mockOnChange = jest.fn();
- render(
- <SearchQueryBuilder
- {...defaultProps}
- onChange={mockOnChange}
- initialQuery="timesSeen:>5"
- />
- );
- await userEvent.click(
- screen.getByRole('button', {name: 'Edit value for filter: timesSeen'})
- );
- await userEvent.clear(
- await screen.findByRole('combobox', {name: 'Edit filter value'})
- );
- await userEvent.keyboard('{enter}');
- // Should have the same value
- expect(
- await screen.findByRole('row', {name: 'timesSeen:>5'})
- ).toBeInTheDocument();
- expect(mockOnChange).not.toHaveBeenCalled();
- });
- it('does not allow invalid values', async function () {
- render(<SearchQueryBuilder {...defaultProps} initialQuery="timesSeen:>100" />);
- await userEvent.click(
- screen.getByRole('button', {name: 'Edit value for filter: timesSeen'})
- );
- await userEvent.keyboard('a{Enter}');
- // Should have the same value because "a" is not a numeric value
- expect(screen.getByRole('row', {name: 'timesSeen:>100'})).toBeInTheDocument();
- await userEvent.keyboard('{Backspace}7k{Enter}');
- // Should accept "7k" as a valid value
- expect(
- await screen.findByRole('row', {name: 'timesSeen:>7k'})
- ).toBeInTheDocument();
- });
- it('can change the operator', async function () {
- render(<SearchQueryBuilder {...defaultProps} initialQuery="timesSeen:>100k" />);
- await userEvent.click(
- screen.getByRole('button', {name: 'Edit operator for filter: timesSeen'})
- );
- await userEvent.click(screen.getByRole('option', {name: 'timesSeen <='}));
- expect(
- await screen.findByRole('row', {name: 'timesSeen:<=100k'})
- ).toBeInTheDocument();
- });
- });
- describe('duration', function () {
- const durationFilterKeys: TagCollection = {
- duration: {
- key: 'duration',
- name: 'Duration',
- },
- };
- const fieldDefinitionGetter: FieldDefinitionGetter = () => ({
- valueType: FieldValueType.DURATION,
- kind: FieldKind.FIELD,
- });
- const durationProps: SearchQueryBuilderProps = {
- ...defaultProps,
- filterKeys: durationFilterKeys,
- filterKeySections: [],
- fieldDefinitionGetter,
- };
- it('new duration filters start with greater than operator and default value', async function () {
- render(<SearchQueryBuilder {...durationProps} />);
- await userEvent.click(getLastInput());
- await userEvent.click(screen.getByRole('option', {name: 'duration'}));
- // Should start with the > operator and a value of 10ms
- expect(
- await screen.findByRole('row', {name: 'duration:>10ms'})
- ).toBeInTheDocument();
- });
- it('duration filters have the correct operator options', async function () {
- render(<SearchQueryBuilder {...durationProps} initialQuery="duration:>100ms" />);
- await userEvent.click(
- screen.getByRole('button', {name: 'Edit operator for filter: duration'})
- );
- expect(
- await screen.findByRole('option', {name: 'duration is'})
- ).toBeInTheDocument();
- expect(screen.getByRole('option', {name: 'duration is not'})).toBeInTheDocument();
- expect(screen.getByRole('option', {name: 'duration >'})).toBeInTheDocument();
- expect(screen.getByRole('option', {name: 'duration <'})).toBeInTheDocument();
- expect(screen.getByRole('option', {name: 'duration >='})).toBeInTheDocument();
- expect(screen.getByRole('option', {name: 'duration <='})).toBeInTheDocument();
- });
- it('duration filters have the correct value suggestions', async function () {
- render(<SearchQueryBuilder {...durationProps} initialQuery="duration:>100ms" />);
- await userEvent.click(
- screen.getByRole('button', {name: 'Edit value for filter: duration'})
- );
- // Default suggestions
- expect(await screen.findByRole('option', {name: '100ms'})).toBeInTheDocument();
- expect(screen.getByRole('option', {name: '100s'})).toBeInTheDocument();
- expect(screen.getByRole('option', {name: '100m'})).toBeInTheDocument();
- expect(screen.getByRole('option', {name: '100h'})).toBeInTheDocument();
- // Entering a number will show unit suggestions for that value
- await userEvent.keyboard('7');
- expect(await screen.findByRole('option', {name: '7ms'})).toBeInTheDocument();
- expect(screen.getByRole('option', {name: '7s'})).toBeInTheDocument();
- expect(screen.getByRole('option', {name: '7m'})).toBeInTheDocument();
- expect(screen.getByRole('option', {name: '7h'})).toBeInTheDocument();
- });
- it('duration filters can change operator', async function () {
- render(<SearchQueryBuilder {...durationProps} initialQuery="duration:>100ms" />);
- await userEvent.click(
- screen.getByRole('button', {name: 'Edit operator for filter: duration'})
- );
- await userEvent.click(await screen.findByRole('option', {name: 'duration <='}));
- expect(
- await screen.findByRole('row', {name: 'duration:<=100ms'})
- ).toBeInTheDocument();
- });
- it('duration filters do not allow invalid values', async function () {
- render(<SearchQueryBuilder {...durationProps} initialQuery="duration:>100ms" />);
- await userEvent.click(
- screen.getByRole('button', {name: 'Edit value for filter: duration'})
- );
- await userEvent.keyboard('a{Enter}');
- // Should have the same value because "a" is not a numeric value
- expect(screen.getByRole('row', {name: 'duration:>100ms'})).toBeInTheDocument();
- await userEvent.keyboard('{Backspace}7m{Enter}');
- // Should accept "7m" as a valid value
- expect(
- await screen.findByRole('row', {name: 'duration:>7m'})
- ).toBeInTheDocument();
- });
- it('duration filters will add a default unit to entered numbers', async function () {
- render(<SearchQueryBuilder {...durationProps} initialQuery="duration:>100ms" />);
- await userEvent.click(
- screen.getByRole('button', {name: 'Edit value for filter: duration'})
- );
- await userEvent.keyboard('7{Enter}');
- // Should accept "7" and add "ms" as the default unit
- expect(
- await screen.findByRole('row', {name: 'duration:>7ms'})
- ).toBeInTheDocument();
- });
- it('keeps previous value when confirming empty value', async function () {
- const mockOnChange = jest.fn();
- render(
- <SearchQueryBuilder
- {...durationProps}
- onChange={mockOnChange}
- initialQuery="duration:>100ms"
- />
- );
- await userEvent.click(
- screen.getByRole('button', {name: 'Edit value for filter: duration'})
- );
- await userEvent.clear(
- await screen.findByRole('combobox', {name: 'Edit filter value'})
- );
- await userEvent.keyboard('{enter}');
- // Should have the same value
- expect(
- await screen.findByRole('row', {name: 'duration:>100ms'})
- ).toBeInTheDocument();
- expect(mockOnChange).not.toHaveBeenCalled();
- });
- });
- describe('percentage', function () {
- const percentageFilterKeys: TagCollection = {
- rate: {
- key: 'rate',
- name: 'rate',
- },
- };
- const fieldDefinitionGetter: FieldDefinitionGetter = () => ({
- valueType: FieldValueType.PERCENTAGE,
- kind: FieldKind.FIELD,
- });
- const percentageProps: SearchQueryBuilderProps = {
- ...defaultProps,
- filterKeys: percentageFilterKeys,
- filterKeySections: [],
- fieldDefinitionGetter,
- };
- it('new percentage filters start with greater than operator and default value', async function () {
- render(<SearchQueryBuilder {...percentageProps} />);
- await userEvent.click(getLastInput());
- await userEvent.click(screen.getByRole('option', {name: 'rate'}));
- // Should start with the > operator and a value of 50%
- expect(await screen.findByRole('row', {name: 'rate:>0.5'})).toBeInTheDocument();
- });
- it('percentage filters have the correct operator options', async function () {
- render(<SearchQueryBuilder {...percentageProps} initialQuery="rate:>0.5" />);
- await userEvent.click(
- screen.getByRole('button', {name: 'Edit operator for filter: rate'})
- );
- expect(await screen.findByRole('option', {name: 'rate is'})).toBeInTheDocument();
- expect(screen.getByRole('option', {name: 'rate is not'})).toBeInTheDocument();
- expect(screen.getByRole('option', {name: 'rate >'})).toBeInTheDocument();
- expect(screen.getByRole('option', {name: 'rate <'})).toBeInTheDocument();
- expect(screen.getByRole('option', {name: 'rate >='})).toBeInTheDocument();
- expect(screen.getByRole('option', {name: 'rate <='})).toBeInTheDocument();
- });
- it('percentage filters can change operator', async function () {
- render(<SearchQueryBuilder {...percentageProps} initialQuery="rate:>0.5" />);
- await userEvent.click(
- screen.getByRole('button', {name: 'Edit operator for filter: rate'})
- );
- await userEvent.click(await screen.findByRole('option', {name: 'rate <='}));
- expect(await screen.findByRole('row', {name: 'rate:<=0.5'})).toBeInTheDocument();
- });
- it('percentage filters do not allow invalid values', async function () {
- render(<SearchQueryBuilder {...percentageProps} initialQuery="rate:>0.5" />);
- await userEvent.click(
- screen.getByRole('button', {name: 'Edit value for filter: rate'})
- );
- await userEvent.keyboard('a{Enter}');
- // Should have the same value because "a" is not a numeric value
- expect(screen.getByRole('row', {name: 'rate:>0.5'})).toBeInTheDocument();
- await userEvent.keyboard('{Backspace}0.2{Enter}');
- // Should accept "0.2" as a valid value
- expect(await screen.findByRole('row', {name: 'rate:>0.2'})).toBeInTheDocument();
- });
- it('percentage filters will convert values with % to ratio', async function () {
- render(<SearchQueryBuilder {...percentageProps} initialQuery="rate:>0.5" />);
- await userEvent.click(
- screen.getByRole('button', {name: 'Edit value for filter: rate'})
- );
- await userEvent.keyboard('70%{Enter}');
- // 70% should be accepted and converted to 0.7
- expect(await screen.findByRole('row', {name: 'rate:>0.7'})).toBeInTheDocument();
- });
- it('keeps previous value when confirming empty value', async function () {
- const mockOnChange = jest.fn();
- render(
- <SearchQueryBuilder
- {...percentageProps}
- onChange={mockOnChange}
- initialQuery="rate:>0.5"
- />
- );
- await userEvent.click(
- screen.getByRole('button', {name: 'Edit value for filter: rate'})
- );
- await userEvent.clear(
- await screen.findByRole('combobox', {name: 'Edit filter value'})
- );
- await userEvent.keyboard('{enter}');
- // Should have the same value
- expect(await screen.findByRole('row', {name: 'rate:>0.5'})).toBeInTheDocument();
- expect(mockOnChange).not.toHaveBeenCalled();
- });
- });
- describe('date', function () {
- // Transpile the lazy-loaded datepicker up front so tests don't flake
- beforeAll(async function () {
- await import('sentry/components/calendar/datePicker');
- });
- it('new date filters start with a value', async function () {
- render(<SearchQueryBuilder {...defaultProps} />);
- await userEvent.click(getLastInput());
- await userEvent.keyboard('age{ArrowDown}{Enter}');
- // Should start with a relative date value
- expect(await screen.findByRole('row', {name: 'age:-24h'})).toBeInTheDocument();
- });
- it('does not allow invalid values', async function () {
- render(<SearchQueryBuilder {...defaultProps} initialQuery="age:-24h" />);
- await userEvent.click(
- screen.getByRole('button', {name: 'Edit value for filter: age'})
- );
- await userEvent.keyboard('a{Enter}');
- // Should have the same value because "a" is not a date value
- expect(screen.getByRole('row', {name: 'age:-24h'})).toBeInTheDocument();
- });
- it('keeps previous value when confirming empty value', async function () {
- const mockOnChange = jest.fn();
- render(
- <SearchQueryBuilder
- {...defaultProps}
- onChange={mockOnChange}
- initialQuery="age:-24h"
- />
- );
- await userEvent.click(
- screen.getByRole('button', {name: 'Edit value for filter: age'})
- );
- await userEvent.clear(
- await screen.findByRole('combobox', {name: 'Edit filter value'})
- );
- await userEvent.keyboard('{enter}');
- // Should have the same value
- expect(await screen.findByRole('row', {name: 'age:-24h'})).toBeInTheDocument();
- expect(mockOnChange).not.toHaveBeenCalled();
- });
- it('shows default date suggestions', async function () {
- render(<SearchQueryBuilder {...defaultProps} initialQuery="age:-24h" />);
- await userEvent.click(
- screen.getByRole('button', {name: 'Edit value for filter: age'})
- );
- await userEvent.click(await screen.findByRole('option', {name: '1 hour ago'}));
- expect(screen.getByRole('row', {name: 'age:-1h'})).toBeInTheDocument();
- });
- it('shows date suggestions when typing', async function () {
- render(<SearchQueryBuilder {...defaultProps} initialQuery="age:-24h" />);
- await userEvent.click(
- screen.getByRole('button', {name: 'Edit value for filter: age'})
- );
- // Typing "7" should show suggestions for 7 minutes, hours, days, and weeks
- await userEvent.keyboard('7');
- await screen.findByRole('option', {name: '7 minutes ago'});
- expect(screen.getByRole('option', {name: '7 hours ago'})).toBeInTheDocument();
- expect(screen.getByRole('option', {name: '7 days ago'})).toBeInTheDocument();
- expect(screen.getByRole('option', {name: '7 weeks ago'})).toBeInTheDocument();
- await userEvent.click(screen.getByRole('option', {name: '7 weeks ago'}));
- expect(screen.getByRole('row', {name: 'age:-7w'})).toBeInTheDocument();
- });
- it('can search before a relative date', async function () {
- render(<SearchQueryBuilder {...defaultProps} initialQuery="age:-24h" />);
- await userEvent.click(
- screen.getByRole('button', {name: 'Edit operator for filter: age'})
- );
- await userEvent.click(await screen.findByRole('option', {name: 'age is before'}));
- // Should flip from "-" to "+"
- expect(await screen.findByRole('row', {name: 'age:+24h'})).toBeInTheDocument();
- });
- it('switches to an absolute date when choosing operator with equality', async function () {
- render(<SearchQueryBuilder {...defaultProps} initialQuery="age:-24h" />);
- await userEvent.click(
- screen.getByRole('button', {name: 'Edit operator for filter: age'})
- );
- await userEvent.click(
- await screen.findByRole('option', {name: 'age is on or after'})
- );
- // Changes operator and fills in the current date (ISO format)
- expect(
- await screen.findByRole('row', {name: 'age:>=2017-10-17T02:41:20.000Z'})
- ).toBeInTheDocument();
- });
- it('can switch from after an absolute date to a relative one', async function () {
- const mockOnChange = jest.fn();
- render(
- <SearchQueryBuilder
- {...defaultProps}
- onChange={mockOnChange}
- initialQuery="foo age:>=2017-10-17"
- />
- );
- await userEvent.click(
- screen.getByRole('button', {name: 'Edit value for filter: age'})
- );
- // Go back to relative date suggestions
- await userEvent.click(await screen.findByRole('button', {name: 'Back'}));
- await userEvent.click(await screen.findByRole('option', {name: '1 hour ago'}));
- // Because relative dates only work with ":", should change the operator to "is after"
- expect(
- within(
- screen.getByRole('button', {name: 'Edit operator for filter: age'})
- ).getByText('is after')
- ).toBeInTheDocument();
- await waitFor(() => {
- expect(mockOnChange).toHaveBeenCalledWith('foo age:-1h', expect.anything());
- });
- });
- it('can switch from before an absolute date to a relative one', async function () {
- const mockOnChange = jest.fn();
- render(
- <SearchQueryBuilder
- {...defaultProps}
- onChange={mockOnChange}
- initialQuery="foo age:<=2017-10-17"
- />
- );
- await userEvent.click(
- screen.getByRole('button', {name: 'Edit value for filter: age'})
- );
- // Go back to relative date suggestions
- await userEvent.click(await screen.findByRole('button', {name: 'Back'}));
- await userEvent.click(await screen.findByRole('option', {name: '1 hour ago'}));
- // Because relative dates only work with ":", should change the operator to "is before"
- expect(
- within(
- screen.getByRole('button', {name: 'Edit operator for filter: age'})
- ).getByText('is before')
- ).toBeInTheDocument();
- await waitFor(() => {
- expect(mockOnChange).toHaveBeenCalledWith('foo age:+1h', expect.anything());
- });
- });
- it('can set an absolute date', async function () {
- const mockOnChange = jest.fn();
- render(
- <SearchQueryBuilder
- {...defaultProps}
- onChange={mockOnChange}
- initialQuery="age:-24h"
- />
- );
- await userEvent.click(
- screen.getByRole('button', {name: 'Edit value for filter: age'})
- );
- await userEvent.click(await screen.findByRole('option', {name: 'Absolute date'}));
- const dateInput = await screen.findByTestId('date-picker');
- await userEvent.type(dateInput, '2017-10-17');
- await userEvent.click(screen.getByRole('button', {name: 'Save'}));
- await waitFor(() => {
- expect(mockOnChange).toHaveBeenCalledWith('age:>2017-10-17', expect.anything());
- });
- });
- it('can set an absolute date with time (UTC)', async function () {
- const mockOnChange = jest.fn();
- render(
- <SearchQueryBuilder
- {...defaultProps}
- onChange={mockOnChange}
- initialQuery="age:>2017-10-17"
- />
- );
- await userEvent.click(
- screen.getByRole('button', {name: 'Edit value for filter: age'})
- );
- await userEvent.click(
- await screen.findByRole('checkbox', {name: 'Include time'})
- );
- await userEvent.click(await screen.findByRole('button', {name: 'Save'}));
- await waitFor(() => {
- expect(mockOnChange).toHaveBeenCalledWith(
- 'age:>2017-10-17T00:00:00Z',
- expect.anything()
- );
- });
- });
- it('can set an absolute date with time (local)', async function () {
- const mockOnChange = jest.fn();
- render(
- <SearchQueryBuilder
- {...defaultProps}
- onChange={mockOnChange}
- initialQuery="age:>2017-10-17"
- />
- );
- await userEvent.click(
- screen.getByRole('button', {name: 'Edit value for filter: age'})
- );
- await userEvent.click(
- await screen.findByRole('checkbox', {name: 'Include time'})
- );
- await userEvent.click(await screen.findByRole('checkbox', {name: 'UTC'}));
- await userEvent.click(await screen.findByRole('button', {name: 'Save'}));
- await waitFor(() => {
- expect(mockOnChange).toHaveBeenCalledWith(
- 'age:>2017-10-17T00:00:00+00:00',
- expect.anything()
- );
- });
- });
- it('displays absolute date value correctly (just date)', function () {
- render(<SearchQueryBuilder {...defaultProps} initialQuery="age:>=2017-10-17" />);
- expect(screen.getByText('is on or after')).toBeInTheDocument();
- expect(screen.getByText('Oct 17')).toBeInTheDocument();
- });
- it('displays absolute date value correctly (with local time)', function () {
- render(
- <SearchQueryBuilder
- {...defaultProps}
- initialQuery="age:>=2017-10-17T14:00:00-00:00"
- />
- );
- expect(screen.getByText('is on or after')).toBeInTheDocument();
- expect(screen.getByText('Oct 17, 2:00 PM')).toBeInTheDocument();
- });
- it('displays absolute date value correctly (with UTC time)', function () {
- render(
- <SearchQueryBuilder
- {...defaultProps}
- initialQuery="age:>=2017-10-17T14:00:00Z"
- />
- );
- expect(screen.getByText('is on or after')).toBeInTheDocument();
- expect(screen.getByText('Oct 17, 2:00 PM UTC')).toBeInTheDocument();
- });
- });
- });
- describe('disallowLogicalOperators', function () {
- it('should mark AND invalid', async function () {
- render(
- <SearchQueryBuilder
- {...defaultProps}
- disallowLogicalOperators
- initialQuery="and"
- />
- );
- expect(screen.getByRole('row', {name: 'and'})).toHaveAttribute(
- 'aria-invalid',
- 'true'
- );
- await userEvent.click(screen.getByRole('row', {name: 'and'}));
- expect(
- await screen.findByText('The AND operator is not allowed in this search')
- ).toBeInTheDocument();
- });
- it('should mark OR invalid', async function () {
- render(
- <SearchQueryBuilder
- {...defaultProps}
- disallowLogicalOperators
- initialQuery="or"
- />
- );
- expect(screen.getByRole('row', {name: 'or'})).toHaveAttribute(
- 'aria-invalid',
- 'true'
- );
- await userEvent.click(screen.getByRole('row', {name: 'or'}));
- expect(
- await screen.findByText('The OR operator is not allowed in this search')
- ).toBeInTheDocument();
- });
- it('should mark parens invalid', async function () {
- render(
- <SearchQueryBuilder
- {...defaultProps}
- disallowLogicalOperators
- initialQuery="()"
- />
- );
- expect(screen.getByRole('row', {name: '('})).toHaveAttribute(
- 'aria-invalid',
- 'true'
- );
- expect(screen.getByRole('row', {name: ')'})).toHaveAttribute(
- 'aria-invalid',
- 'true'
- );
- await userEvent.click(screen.getByRole('row', {name: '('}));
- expect(
- await screen.findByText('Parentheses are not supported in this search')
- ).toBeInTheDocument();
- });
- });
- describe('disallowWildcard', function () {
- it('should mark tokens with wildcards invalid', async function () {
- render(
- <SearchQueryBuilder
- {...defaultProps}
- disallowWildcard
- initialQuery="browser.name:Firefox*"
- />
- );
- expect(screen.getByRole('row', {name: 'browser.name:Firefox*'})).toHaveAttribute(
- 'aria-invalid',
- 'true'
- );
- // Put focus into token, should show error message
- await userEvent.click(getLastInput());
- await userEvent.keyboard('{ArrowLeft}');
- expect(
- await screen.findByText('Wildcards not supported in search')
- ).toBeInTheDocument();
- });
- it('should mark free text with wildcards invalid', async function () {
- render(
- <SearchQueryBuilder {...defaultProps} disallowWildcard initialQuery="foo*" />
- );
- expect(screen.getByRole('row', {name: 'foo*'})).toHaveAttribute(
- 'aria-invalid',
- 'true'
- );
- await userEvent.click(getLastInput());
- expect(
- await screen.findByText('Wildcards not supported in search')
- ).toBeInTheDocument();
- });
- });
- describe('disallowFreeText', function () {
- it('should mark free text invalid', async function () {
- render(
- <SearchQueryBuilder {...defaultProps} disallowFreeText initialQuery="foo" />
- );
- expect(screen.getByRole('row', {name: 'foo'})).toHaveAttribute(
- 'aria-invalid',
- 'true'
- );
- await userEvent.click(getLastInput());
- expect(
- await screen.findByText('Free text is not supported in this search')
- ).toBeInTheDocument();
- });
- });
- describe('highlightUnsupportedFilters', function () {
- it('should mark unsupported filters as invalid', async function () {
- render(
- <SearchQueryBuilder
- {...defaultProps}
- disallowUnsupportedFilters
- initialQuery="foo:bar"
- />
- );
- expect(screen.getByRole('row', {name: 'foo:bar'})).toHaveAttribute(
- 'aria-invalid',
- 'true'
- );
- await userEvent.click(getLastInput());
- await userEvent.keyboard('{ArrowLeft}');
- expect(
- await screen.findByText('Invalid key. "foo" is not a supported search key.')
- ).toBeInTheDocument();
- });
- });
- describe('invalidMessages', function () {
- it('should customize invalid messages', async function () {
- render(
- <SearchQueryBuilder
- {...defaultProps}
- initialQuery="foo:"
- invalidMessages={{
- [InvalidReason.FILTER_MUST_HAVE_VALUE]: 'foo bar baz',
- }}
- />
- );
- expect(screen.getByRole('row', {name: 'foo:'})).toHaveAttribute(
- 'aria-invalid',
- 'true'
- );
- await userEvent.click(getLastInput());
- await userEvent.keyboard('{ArrowLeft}');
- expect(await screen.findByText('foo bar baz')).toBeInTheDocument();
- });
- });
- });
|