UNPKG

@shopify/cli-kit

Version:

A set of utilities, interfaces, and models that are common across all the platform features

680 lines (598 loc) • 40 kB
import { AutocompletePrompt } from './AutocompletePrompt.js'; import { getLastFrameAfterUnmount, sendInputAndWait, sendInputAndWaitForChange, sendInputAndWaitForContent, waitForInputsToBeReady, render, } from '../../testing/ui.js'; import { Stdout } from '../../ui.js'; import { AbortController } from '../../../../public/node/abort.js'; import { beforeEach, describe, expect, test, vi } from 'vitest'; import React from 'react'; import { useStdout } from 'ink'; vi.mock('ink', async () => { const original = await vi.importActual('ink'); return { ...original, useStdout: vi.fn(), }; }); const ARROW_DOWN = '\u001B[B'; const ENTER = '\r'; const DELETE = '\u007F'; const DATABASE = [ { label: 'first', value: 'first' }, { label: 'second', value: 'second' }, { label: 'third', value: 'third' }, { label: 'fourth', value: 'fourth' }, { label: 'fifth', value: 'fifth' }, { label: 'sixth', value: 'sixth' }, { label: 'seventh', value: 'seventh' }, { label: 'eighth', value: 'eighth' }, { label: 'ninth', value: 'ninth' }, { label: 'tenth', value: 'tenth' }, { label: 'eleventh', value: 'eleventh' }, { label: 'twelfth', value: 'twelfth' }, { label: 'thirteenth', value: 'thirteenth' }, { label: 'fourteenth', value: 'fourteenth' }, { label: 'fifteenth', value: 'fifteenth' }, { label: 'sixteenth', value: 'sixteenth' }, { label: 'seventeenth', value: 'seventeenth' }, { label: 'eighteenth', value: 'eighteenth' }, { label: 'nineteenth', value: 'nineteenth' }, { label: 'twentieth', value: 'twentieth' }, { label: 'twenty-first', value: 'twenty-first' }, { label: 'twenty-second', value: 'twenty-second' }, { label: 'twenty-third', value: 'twenty-third' }, { label: 'twenty-fourth', value: 'twenty-fourth' }, { label: 'twenty-fifth', value: 'twenty-fifth' }, { label: 'twenty-sixth', value: 'twenty-sixth' }, { label: 'twenty-seventh', value: 'twenty-seventh' }, { label: 'twenty-eighth', value: 'twenty-eighth' }, { label: 'twenty-ninth', value: 'twenty-ninth' }, { label: 'thirtieth', value: 'thirtieth' }, { label: 'thirty-first', value: 'thirty-first' }, { label: 'thirty-second', value: 'thirty-second' }, { label: 'thirty-third', value: 'thirty-third' }, { label: 'thirty-fourth', value: 'thirty-fourth' }, { label: 'thirty-fifth', value: 'thirty-fifth' }, { label: 'thirty-sixth', value: 'thirty-sixth' }, { label: 'thirty-seventh', value: 'thirty-seventh' }, { label: 'thirty-eighth', value: 'thirty-eighth' }, { label: 'thirty-ninth', value: 'thirty-ninth' }, { label: 'fortieth', value: 'fortieth' }, { label: 'forty-first', value: 'forty-first' }, { label: 'forty-second', value: 'forty-second' }, { label: 'forty-third', value: 'forty-third' }, { label: 'forty-fourth', value: 'forty-fourth' }, { label: 'forty-fifth', value: 'forty-fifth' }, { label: 'forty-sixth', value: 'forty-sixth' }, { label: 'forty-seventh', value: 'forty-seventh' }, { label: 'forty-eighth', value: 'forty-eighth' }, { label: 'forty-ninth', value: 'forty-ninth' }, { label: 'fiftieth', value: 'fiftieth' }, ]; beforeEach(() => { vi.mocked(useStdout).mockReturnValue({ stdout: new Stdout({ columns: 80, rows: 80, }), write: () => { }, }); }); describe('AutocompletePrompt', async () => { test('choose an answer', async () => { const onEnter = vi.fn(); const items = [ { label: 'first', value: 'first' }, { label: 'second', value: 'second' }, { label: 'third', value: 'third' }, ]; const infoTable = { Add: ['new-ext'], Remove: ['integrated-demand-ext', 'order-discount'] }; const renderInstance = render(React.createElement(AutocompletePrompt, { message: ['Associate your project with the org', { userInput: 'Castile Ventures' }, { char: '?' }], choices: items, infoTable: infoTable, onSubmit: onEnter, search: () => Promise.resolve({ data: [], }) })); await waitForInputsToBeReady(); await sendInputAndWaitForChange(renderInstance, ARROW_DOWN); await sendInputAndWaitForChange(renderInstance, ENTER); expect(getLastFrameAfterUnmount(renderInstance)).toMatchInlineSnapshot(` "? Associate your project with the org Castile Ventures? ✔ second " `); expect(onEnter).toHaveBeenCalledWith(items[1].value); }); test('renders groups', async () => { const items = [ { label: 'first', value: 'first', group: 'Automations' }, { label: 'second', value: 'second', group: 'Automations' }, { label: 'third', value: 'third', group: 'Merchant Admin' }, { label: 'fourth', value: 'fourth', group: 'Merchant Admin' }, { label: 'fifth', value: 'fifth' }, { label: 'sixth', value: 'sixth' }, { label: 'seventh', value: 'seventh' }, { label: 'eighth', value: 'eighth' }, { label: 'ninth', value: 'ninth' }, { label: 'tenth', value: 'tenth' }, ]; const renderInstance = render(React.createElement(AutocompletePrompt, { message: "Associate your project with the org Castile Ventures?", choices: items, onSubmit: () => { }, search: () => Promise.resolve({ data: [], }) })); expect(renderInstance.lastFrame()).toMatchInlineSnapshot(` "? Associate your project with the org Castile Ventures? Type to search... Automations > first second Merchant Admin third fourth Other fifth sixth seventh eighth ninth tenth Press ↑↓ arrows to select, enter to confirm. " `); }); test('supports an info table', async () => { const items = [ { label: 'first', value: 'first' }, { label: 'second', value: 'second' }, { label: 'third', value: 'third' }, { label: 'fourth', value: 'fourth' }, ]; const infoTable = { Add: ['new-ext'], Remove: ['integrated-demand-ext', 'order-discount'], }; const renderInstance = render(React.createElement(AutocompletePrompt, { message: "Associate your project with the org Castile Ventures?", choices: items, infoTable: infoTable, onSubmit: () => { }, search: () => Promise.resolve({ data: [] }) })); expect(renderInstance.lastFrame()).toMatchInlineSnapshot(` "? Associate your project with the org Castile Ventures? ┃ \u001b[1mAdd\u001b[22m ┃ • new-ext ┃ ┃ \u001b[1mRemove\u001b[22m ┃ • integrated-demand-ext ┃ • order-discount > first second third fourth Press ↑↓ arrows to select, enter to confirm. " `); }); test('supports an info message', async () => { const items = [ { label: 'first', value: 'first' }, { label: 'second', value: 'second' }, { label: 'third', value: 'third' }, { label: 'fourth', value: 'fourth' }, ]; const infoMessage = { title: { color: 'red', text: 'Info message title', }, body: 'Info message body', }; const renderInstance = render(React.createElement(AutocompletePrompt, { message: "Associate your project with the org Castile Ventures?", choices: items, infoMessage: infoMessage, onSubmit: () => { }, search: () => Promise.resolve({ data: [] }) })); expect(renderInstance.lastFrame()).toMatchInlineSnapshot(` "? Associate your project with the org Castile Ventures? ┃ Info message title ┃ ┃ Info message body > first second third fourth Press ↑↓ arrows to select, enter to confirm. " `); }); test("doesn't submit if there are no choices", async () => { const onEnter = vi.fn(); const searchPromise = Promise.resolve({ data: [], }); const search = () => { return searchPromise; }; const renderInstance = render(React.createElement(AutocompletePrompt, { message: "Associate your project with the org Castile Ventures?", choices: DATABASE, onSubmit: onEnter, search: search })); await waitForInputsToBeReady(); await sendInputAndWaitForContent(renderInstance, 'No results found', 'a'); // prompt doesn't change when enter is pressed await sendInputAndWait(renderInstance, 100, ENTER); expect(renderInstance.lastFrame()).toMatchInlineSnapshot(` "? Associate your project with the org Castile Ventures? a█ No results found. Try again with a different keyword. " `); expect(onEnter).not.toHaveBeenCalled(); }); test('has a loading state', async () => { const onEnter = vi.fn(); const search = () => { return new Promise((resolve) => { setTimeout(() => { resolve({ data: [{ label: 'a', value: 'b' }] }); }, 2000); }); }; const renderInstance = render(React.createElement(AutocompletePrompt, { message: "Associate your project with the org Castile Ventures?", choices: DATABASE, onSubmit: onEnter, search: search })); await waitForInputsToBeReady(); await sendInputAndWaitForContent(renderInstance, 'Loading...', 'a'); // prompt doesn't change when enter is pressed await new Promise((resolve) => setTimeout(resolve, 100)); await sendInputAndWait(renderInstance, 100, ENTER); expect(renderInstance.lastFrame()).toMatchInlineSnapshot(` "? Associate your project with the org Castile Ventures? a█ Loading... " `); expect(onEnter).not.toHaveBeenCalled(); }); test('allows searching with pagination', async () => { const onEnter = vi.fn(); const search = async (term) => { return { data: DATABASE.filter((item) => item.label.includes(term)), }; }; const renderInstance = render(React.createElement(AutocompletePrompt, { message: "Associate your project with the org Castile Ventures?", choices: DATABASE, onSubmit: onEnter, search: search })); expect(renderInstance.lastFrame()).toMatchInlineSnapshot(` "? Associate your project with the org Castile Ventures? Type to search... > first   second   third   fourth   fifth   sixth   seventh   eighth   ninth   tenth   eleventh   twelfth   thirteenth   fourteenth   fifteenth   sixteenth   seventeenth   eighteenth   nineteenth   twentieth   twenty-first   twenty-second   twenty-third   twenty-fourth   twenty-fifth   Press ↑↓ arrows to select, enter to confirm. " `); await waitForInputsToBeReady(); await sendInputAndWaitForContent(renderInstance, 'thirty-sixth', 'i'); expect(renderInstance.lastFrame()).toMatchInlineSnapshot(` "? Associate your project with the org Castile Ventures? i█ > first   third   fifth   sixth   eighth   ninth   thirteenth   fifteenth   sixteenth   eighteenth   nineteenth   twentieth   twenty-first   twenty-third   twenty-fifth   twenty-sixth   twenty-eighth   twenty-ninth   thirtieth   thirty-first   thirty-second   thirty-third   thirty-fourth   thirty-fifth   thirty-sixth   Press ↑↓ arrows to select, enter to confirm. " `); await sendInputAndWaitForChange(renderInstance, DELETE); expect(renderInstance.lastFrame()).toMatchInlineSnapshot(` "? Associate your project with the org Castile Ventures? Type to search... > first   second   third   fourth   fifth   sixth   seventh   eighth   ninth   tenth   eleventh   twelfth   thirteenth   fourteenth   fifteenth   sixteenth   seventeenth   eighteenth   nineteenth   twentieth   twenty-first   twenty-second   twenty-third   twenty-fourth   twenty-fifth   Press ↑↓ arrows to select, enter to confirm. " `); await sendInputAndWaitForContent(renderInstance, 'thirty-sixth', 'i'); await sendInputAndWaitForChange(renderInstance, ARROW_DOWN); await sendInputAndWaitForChange(renderInstance, ARROW_DOWN); expect(renderInstance.lastFrame()).toMatchInlineSnapshot(` "? Associate your project with the org Castile Ventures? i█ first   third   > fifth   sixth   eighth   ninth   thirteenth   fifteenth   sixteenth   eighteenth   nineteenth   twentieth   twenty-first   twenty-third   twenty-fifth   twenty-sixth   twenty-eighth   twenty-ninth   thirtieth   thirty-first   thirty-second   thirty-third   thirty-fourth   thirty-fifth   thirty-sixth   Press ↑↓ arrows to select, enter to confirm. " `); await sendInputAndWaitForChange(renderInstance, ENTER); expect(getLastFrameAfterUnmount(renderInstance)).toMatchInlineSnapshot(` "? Associate your project with the org Castile Ventures? ✔ fifth " `); expect(onEnter).toHaveBeenCalledWith('fifth'); }); test('allows searching with malformed regex', async () => { const onEnter = vi.fn(); const db = [...DATABASE, { label: 'with\\slash', value: 'with\\slash' }]; const search = async (term) => { return { data: db.filter((item) => item.label.includes(term)), }; }; const renderInstance = render(React.createElement(AutocompletePrompt, { message: "Associate your project with the org Castile Ventures?", choices: db, onSubmit: onEnter, search: search })); expect(renderInstance.lastFrame()).toMatchInlineSnapshot(` "? Associate your project with the org Castile Ventures? Type to search... > first   second   third   fourth   fifth   sixth   seventh   eighth   ninth   tenth   eleventh   twelfth   thirteenth   fourteenth   fifteenth   sixteenth   seventeenth   eighteenth   nineteenth   twentieth   twenty-first   twenty-second   twenty-third   twenty-fourth   twenty-fifth   Press ↑↓ arrows to select, enter to confirm. " `); await waitForInputsToBeReady(); await sendInputAndWaitForContent(renderInstance, 'slash', '\\'); expect(renderInstance.lastFrame()).toMatchInlineSnapshot(` "? Associate your project with the org Castile Ventures? \\\\█ > with\\\\slash Press ↑↓ arrows to select, enter to confirm. " `); }); test('displays an error message if the search fails', async () => { const search = (_term) => { return Promise.reject(new Error('Something went wrong')); }; const renderInstance = render(React.createElement(AutocompletePrompt, { message: "Associate your project with the org Castile Ventures?", choices: DATABASE, onSubmit: () => { }, search: search })); await waitForInputsToBeReady(); await sendInputAndWaitForContent(renderInstance, 'There has been an error', 'i'); expect(renderInstance.lastFrame()).toMatchInlineSnapshot(` "? Associate your project with the org Castile Ventures? i█ There has been an error while searching. Please try again later. " `); }); test('immediately shows the initial items if the search is empty', async () => { const search = (term) => { return Promise.resolve({ data: DATABASE.filter((item) => item.label.includes(term)), }); }; const renderInstance = render(React.createElement(AutocompletePrompt, { message: "Associate your project with the org Castile Ventures?", choices: DATABASE, onSubmit: () => { }, search: search })); await waitForInputsToBeReady(); await sendInputAndWaitForContent(renderInstance, 'thirty-sixth', 'i'); expect(renderInstance.lastFrame()).toMatchInlineSnapshot(` "? Associate your project with the org Castile Ventures? i█ > first   third   fifth   sixth   eighth   ninth   thirteenth   fifteenth   sixteenth   eighteenth   nineteenth   twentieth   twenty-first   twenty-third   twenty-fifth   twenty-sixth   twenty-eighth   twenty-ninth   thirtieth   thirty-first   thirty-second   thirty-third   thirty-fourth   thirty-fifth   thirty-sixth   Press ↑↓ arrows to select, enter to confirm. " `); await sendInputAndWaitForChange(renderInstance, DELETE); expect(renderInstance.lastFrame()).toMatchInlineSnapshot(` "? Associate your project with the org Castile Ventures? Type to search... > first   second   third   fourth   fifth   sixth   seventh   eighth   ninth   tenth   eleventh   twelfth   thirteenth   fourteenth   fifteenth   sixteenth   seventeenth   eighteenth   nineteenth   twentieth   twenty-first   twenty-second   twenty-third   twenty-fourth   twenty-fifth   Press ↑↓ arrows to select, enter to confirm. " `); }); test('shows a message that indicates there are more results than shown', async () => { const search = (_term) => { return Promise.resolve({ data: DATABASE, meta: { hasNextPage: true }, }); }; const renderInstance = render(React.createElement(AutocompletePrompt, { message: "Associate your project with the org Castile Ventures?", choices: DATABASE, onSubmit: () => { }, hasMorePages: true, search: search })); expect(renderInstance.lastFrame()).toMatchInlineSnapshot(` "? Associate your project with the org Castile Ventures? Type to search... > first   second   third   fourth   fifth   sixth   seventh   eighth   ninth   tenth   eleventh   twelfth   thirteenth   fourteenth   fifteenth   sixteenth   seventeenth   eighteenth   nineteenth   twentieth   twenty-first   twenty-second   twenty-third   twenty-fourth   twenty-fifth   Press ↑↓ arrows to select, enter to confirm. 1-50 of many Find what you're looking for by typing its name. " `); }); test('adapts to the height of the container', async () => { vi.mocked(useStdout).mockReturnValue({ stdout: new Stdout({ rows: 10 }), write: () => { }, }); const items = [ { label: 'first', value: 'first', group: 'Automations' }, { label: 'second', value: 'second', group: 'Automations' }, { label: 'third', value: 'third', group: 'Merchant Admin' }, { label: 'fourth', value: 'fourth', group: 'Merchant Admin' }, { label: 'fifth', value: 'fifth' }, { label: 'sixth', value: 'sixth' }, { label: 'seventh', value: 'seventh' }, { label: 'eighth', value: 'eighth' }, { label: 'ninth', value: 'ninth' }, { label: 'tenth', value: 'tenth' }, ]; const renderInstance = render(React.createElement(AutocompletePrompt, { message: "Associate your project with the org Castile Ventures?", choices: items, onSubmit: () => { }, hasMorePages: true, search: () => Promise.resolve({ data: items, }) })); expect(renderInstance.lastFrame()).toMatchInlineSnapshot(` "? Associate your project with the org Castile Ventures? Type to search... Automations \u001b[46m \u001b[49m > first \u001b[100m \u001b[49m second \u001b[100m \u001b[49m \u001b[100m \u001b[49m Merchant Admin \u001b[100m \u001b[49m Press ↑↓ arrows to select, enter to confirm. 1-10 of many Find what you're looking for by typing its name. " `); }); test('abortController can be used to exit the prompt from outside', async () => { const items = [ { label: 'a', value: 'a' }, { label: 'b', value: 'b' }, ]; const abortController = new AbortController(); const renderInstance = render(React.createElement(AutocompletePrompt, { message: "Associate your project with the org Castile Ventures?", choices: items, onSubmit: () => { }, search: () => Promise.resolve({ data: [], }), abortSignal: abortController.signal })); const promise = renderInstance.waitUntilExit(); abortController.abort(); // wait for the onAbort promise to resolve await new Promise((resolve) => setTimeout(resolve, 0)); expect(getLastFrameAfterUnmount(renderInstance)).toEqual(''); await expect(promise).resolves.toEqual(undefined); }); }); //# sourceMappingURL=AutocompletePrompt.test.js.map