diff --git a/e2e/playwright/export-snapshots/gltf-binary.png b/e2e/playwright/export-snapshots/gltf-binary.png index 940c9c20f..93a834994 100644 Binary files a/e2e/playwright/export-snapshots/gltf-binary.png and b/e2e/playwright/export-snapshots/gltf-binary.png differ diff --git a/e2e/playwright/export-snapshots/gltf-embedded.png b/e2e/playwright/export-snapshots/gltf-embedded.png index 940c9c20f..93a834994 100644 Binary files a/e2e/playwright/export-snapshots/gltf-embedded.png and b/e2e/playwright/export-snapshots/gltf-embedded.png differ diff --git a/e2e/playwright/export-snapshots/gltf-standard.png b/e2e/playwright/export-snapshots/gltf-standard.png index 940c9c20f..93a834994 100644 Binary files a/e2e/playwright/export-snapshots/gltf-standard.png and b/e2e/playwright/export-snapshots/gltf-standard.png differ diff --git a/e2e/playwright/export-snapshots/obj-.png b/e2e/playwright/export-snapshots/obj-.png index a4f5783fa..136a04ccb 100644 Binary files a/e2e/playwright/export-snapshots/obj-.png and b/e2e/playwright/export-snapshots/obj-.png differ diff --git a/e2e/playwright/export-snapshots/ply-ascii.png b/e2e/playwright/export-snapshots/ply-ascii.png index a4f5783fa..136a04ccb 100644 Binary files a/e2e/playwright/export-snapshots/ply-ascii.png and b/e2e/playwright/export-snapshots/ply-ascii.png differ diff --git a/e2e/playwright/export-snapshots/ply-binary_big_endian.png b/e2e/playwright/export-snapshots/ply-binary_big_endian.png index a4f5783fa..136a04ccb 100644 Binary files a/e2e/playwright/export-snapshots/ply-binary_big_endian.png and b/e2e/playwright/export-snapshots/ply-binary_big_endian.png differ diff --git a/e2e/playwright/export-snapshots/ply-binary_little_endian.png b/e2e/playwright/export-snapshots/ply-binary_little_endian.png index a4f5783fa..136a04ccb 100644 Binary files a/e2e/playwright/export-snapshots/ply-binary_little_endian.png and b/e2e/playwright/export-snapshots/ply-binary_little_endian.png differ diff --git a/e2e/playwright/export-snapshots/step-.png b/e2e/playwright/export-snapshots/step-.png index 940c9c20f..93a834994 100644 Binary files a/e2e/playwright/export-snapshots/step-.png and b/e2e/playwright/export-snapshots/step-.png differ diff --git a/e2e/playwright/export-snapshots/stl-ascii.png b/e2e/playwright/export-snapshots/stl-ascii.png index a4f5783fa..136a04ccb 100644 Binary files a/e2e/playwright/export-snapshots/stl-ascii.png and b/e2e/playwright/export-snapshots/stl-ascii.png differ diff --git a/e2e/playwright/export-snapshots/stl-binary.png b/e2e/playwright/export-snapshots/stl-binary.png index a4f5783fa..136a04ccb 100644 Binary files a/e2e/playwright/export-snapshots/stl-binary.png and b/e2e/playwright/export-snapshots/stl-binary.png differ diff --git a/e2e/playwright/test-utils.ts b/e2e/playwright/test-utils.ts index bff42b02c..795ec5cac 100644 --- a/e2e/playwright/test-utils.ts +++ b/e2e/playwright/test-utils.ts @@ -12,7 +12,7 @@ import pixelMatch from 'pixelmatch' import { PNG } from 'pngjs' import { Protocol } from 'playwright-core/types/protocol' import type { Models } from '@kittycad/lib' -import { APP_NAME } from 'lib/constants' +import { APP_NAME, COOKIE_NAME } from 'lib/constants' import waitOn from 'wait-on' import { secrets } from './secrets' import { TEST_SETTINGS_KEY, TEST_SETTINGS } from './storageStates' @@ -643,6 +643,16 @@ export async function setup(context: BrowserContext, page: Page) { settings: TOML.stringify({ settings: TEST_SETTINGS }), } ) + + await context.addCookies([ + { + name: COOKIE_NAME, + value: secrets.token, + path: '/', + domain: 'localhost', + secure: true, + }, + ]) // kill animations, speeds up tests and reduced flakiness await page.emulateMedia({ reducedMotion: 'reduce' }) } diff --git a/e2e/playwright/text-to-cad-tests.spec.ts b/e2e/playwright/text-to-cad-tests.spec.ts new file mode 100644 index 000000000..4a532a670 --- /dev/null +++ b/e2e/playwright/text-to-cad-tests.spec.ts @@ -0,0 +1,358 @@ +import { test, expect, Page } from '@playwright/test' + +import { getUtils, setup, tearDown } from './test-utils' + +test.beforeEach(async ({ context, page }) => { + await setup(context, page) +}) + +test.afterEach(async ({ page }, testInfo) => { + await tearDown(page, testInfo) +}) + +const CtrlKey = process.platform === 'darwin' ? 'Meta' : 'Control' + +test.describe('Text-to-CAD tests', () => { + test('basic lego happy case', async ({ page }) => { + const u = await getUtils(page) + + await page.setViewportSize({ width: 1000, height: 500 }) + + await u.waitForAuthSkipAppStart() + + await sendPromptFromCommandBar(page, 'a 2x4 lego') + + // Find the toast. + // Look out for the toast message + const submittingToastMessage = page.getByText( + `Submitting to Text-to-CAD API...` + ) + await expect(submittingToastMessage).toBeVisible() + + await page.waitForTimeout(5000) + + const generatingToastMessage = page.getByText( + `Generating parametric model...` + ) + await expect(generatingToastMessage).toBeVisible() + + const successToastMessage = page.getByText(`Text-to-CAD successful`) + await expect(successToastMessage).toBeVisible() + + await expect(page.getByText('Copied')).not.toBeVisible() + + // Hit copy to clipboard. + const copyToClipboardButton = page.getByRole('button', { + name: 'Copy to clipboard', + }) + await expect(copyToClipboardButton).toBeVisible() + + await copyToClipboardButton.click() + + // Expect the code to be copied. + await expect(page.getByText('Copied')).toBeVisible() + + // Click in the code editor. + await page.locator('.cm-content').click() + + // Paste the code. + await page.keyboard.down(CtrlKey) + await page.keyboard.press('KeyV') + await page.keyboard.up(CtrlKey) + + // Expect the code to be pasted. + await expect(page.locator('.cm-content')).toContainText(`const`) + + // make sure a model renders. + // wait for execution done + await u.openDebugPanel() + await u.expectCmdLog('[data-message-type="execution-done"]') + await u.closeDebugPanel() + + // Find the toast close button. + const closeButton = page.getByRole('button', { name: 'Close' }) + await expect(closeButton).toBeVisible() + await closeButton.click() + + // The toast should disappear. + await expect(successToastMessage).not.toBeVisible() + }) + + test('you can reject text-to-cad output and it does nothing', async ({ + page, + }) => { + const u = await getUtils(page) + + await page.setViewportSize({ width: 1000, height: 500 }) + + await u.waitForAuthSkipAppStart() + + await sendPromptFromCommandBar(page, 'a 2x4 lego') + + // Find the toast. + // Look out for the toast message + const submittingToastMessage = page.getByText( + `Submitting to Text-to-CAD API...` + ) + await expect(submittingToastMessage).toBeVisible() + + await page.waitForTimeout(5000) + + const generatingToastMessage = page.getByText( + `Generating parametric model...` + ) + await expect(generatingToastMessage).toBeVisible() + + const successToastMessage = page.getByText(`Text-to-CAD successful`) + await expect(successToastMessage).toBeVisible() + + // Hit copy to clipboard. + const rejectButton = page.getByRole('button', { name: 'Reject' }) + await expect(rejectButton).toBeVisible() + + await rejectButton.click() + + // The toast should disappear. + await expect(successToastMessage).not.toBeVisible() + + // Expect no code. + await expect(page.locator('.cm-content')).toContainText(``) + }) + + test('sending a bad prompt fails, can dismiss', async ({ page }) => { + const u = await getUtils(page) + + await page.setViewportSize({ width: 1000, height: 500 }) + + await u.waitForAuthSkipAppStart() + + const commandBarButton = page.getByRole('button', { name: 'Commands' }) + await expect(commandBarButton).toBeVisible() + // Click the command bar button + await commandBarButton.click() + + // Wait for the command bar to appear + const cmdSearchBar = page.getByPlaceholder('Search commands') + await expect(cmdSearchBar).toBeVisible() + + const textToCadCommand = page.getByText('Text-to-CAD') + await expect(textToCadCommand.first()).toBeVisible() + // Click the Text-to-CAD command + await textToCadCommand.first().click() + + // Enter the prompt. + const prompt = page.getByText('Prompt') + await expect(prompt.first()).toBeVisible() + + // Type the prompt. + await page.keyboard.type( + 'akjsndladf lajbhflauweyfa;wieufjn;wieJNUF;.wjdfn weh Fwhefb' + ) + await page.waitForTimeout(1000) + await page.keyboard.press('Enter') + + // Find the toast. + // Look out for the toast message + const submittingToastMessage = page.getByText( + `Submitting to Text-to-CAD API...` + ) + await expect(submittingToastMessage).toBeVisible() + + const generatingToastMessage = page.getByText( + `Generating parametric model...` + ) + await expect(generatingToastMessage).toBeVisible() + + const failureToastMessage = page.getByText( + `The prompt must clearly describe a CAD model` + ) + await expect(failureToastMessage).toBeVisible() + + await page.waitForTimeout(1000) + + // Make sure the toast did not say it was successful. + const successToastMessage = page.getByText(`Text-to-CAD successful`) + await expect(successToastMessage).not.toBeVisible() + await expect(page.getByText(`Text-to-CAD failed`)).toBeVisible() + + // Find the toast dismiss button. + const dismissButton = page.getByRole('button', { name: 'Dismiss' }) + await expect(dismissButton).toBeVisible() + await dismissButton.click() + + // The toast should disappear. + await expect(failureToastMessage).not.toBeVisible() + }) + + test('sending a bad prompt fails, can start over', async ({ page }) => { + const u = await getUtils(page) + + await page.setViewportSize({ width: 1000, height: 500 }) + + await u.waitForAuthSkipAppStart() + + const commandBarButton = page.getByRole('button', { name: 'Commands' }) + await expect(commandBarButton).toBeVisible() + // Click the command bar button + await commandBarButton.click() + + // Wait for the command bar to appear + const cmdSearchBar = page.getByPlaceholder('Search commands') + await expect(cmdSearchBar).toBeVisible() + + const textToCadCommand = page.getByText('Text-to-CAD') + await expect(textToCadCommand.first()).toBeVisible() + // Click the Text-to-CAD command + await textToCadCommand.first().click() + + // Enter the prompt. + const prompt = page.getByText('Prompt') + await expect(prompt.first()).toBeVisible() + + const badPrompt = + 'akjsndladf lajbhflauweyfa;wieufjn;wieJNUF;.wjdfn weh Fwhefb' + + // Type the prompt. + await page.keyboard.type(badPrompt) + await page.waitForTimeout(1000) + await page.keyboard.press('Enter') + + // Find the toast. + // Look out for the toast message + const submittingToastMessage = page.getByText( + `Submitting to Text-to-CAD API...` + ) + await expect(submittingToastMessage).toBeVisible() + + const generatingToastMessage = page.getByText( + `Generating parametric model...` + ) + await expect(generatingToastMessage).toBeVisible() + + const failureToastMessage = page.getByText( + `The prompt must clearly describe a CAD model` + ) + await expect(failureToastMessage).toBeVisible() + + await page.waitForTimeout(1000) + + // Make sure the toast did not say it was successful. + const successToastMessage = page.getByText(`Text-to-CAD successful`) + await expect(successToastMessage).not.toBeVisible() + await expect(page.getByText(`Text-to-CAD failed`)).toBeVisible() + + // Click the edit prompt button to try again. + const editPromptButton = page.getByRole('button', { name: 'Edit prompt' }) + await expect(editPromptButton).toBeVisible() + await editPromptButton.click() + + // The toast should disappear. + await expect(failureToastMessage).not.toBeVisible() + + // Make sure the old prompt is still there and can be edited. + await expect(page.locator('textarea')).toContainText(badPrompt) + + // Select all and start a new prompt. + await page.keyboard.down(CtrlKey) + await page.keyboard.press('KeyA') + await page.keyboard.up(CtrlKey) + await page.keyboard.type('a 2x4 lego') + + // Submit the new prompt. + await page.keyboard.press('Enter') + + // Make sure the new prompt works. + // Find the toast. + // Look out for the toast message + await expect(submittingToastMessage).toBeVisible() + + await page.waitForTimeout(5000) + + await expect(generatingToastMessage).toBeVisible() + + await expect(successToastMessage).toBeVisible() + }) + + test('ensure you can shift+enter in the prompt box', async ({ page }) => { + const u = await getUtils(page) + + await page.setViewportSize({ width: 1000, height: 500 }) + + await u.waitForAuthSkipAppStart() + + const promptWithNewline = `a 2x4\nlego` + + const commandBarButton = page.getByRole('button', { name: 'Commands' }) + await expect(commandBarButton).toBeVisible() + // Click the command bar button + await commandBarButton.click() + + // Wait for the command bar to appear + const cmdSearchBar = page.getByPlaceholder('Search commands') + await expect(cmdSearchBar).toBeVisible() + + const textToCadCommand = page.getByText('Text-to-CAD') + await expect(textToCadCommand.first()).toBeVisible() + // Click the Text-to-CAD command + await textToCadCommand.first().click() + + // Enter the prompt. + const prompt = page.getByText('Prompt') + await expect(prompt.first()).toBeVisible() + + // Type the prompt. + await page.keyboard.type('a 2x4') + await page.waitForTimeout(1000) + await page.keyboard.down('Shift') + await page.keyboard.press('Enter') + await page.keyboard.up('Shift') + await page.keyboard.type('lego') + await page.waitForTimeout(1000) + await page.keyboard.press('Enter') + + // Find the toast. + // Look out for the toast message + const submittingToastMessage = page.getByText( + `Submitting to Text-to-CAD API...` + ) + await expect(submittingToastMessage).toBeVisible() + + await page.waitForTimeout(1000) + + const generatingToastMessage = page.getByText( + `Generating parametric model...` + ) + await expect(generatingToastMessage).toBeVisible() + await page.waitForTimeout(5000) + + const successToastMessage = page.getByText(`Text-to-CAD successful`) + await expect(successToastMessage).toBeVisible() + + await expect(page.getByText(promptWithNewline)).toBeVisible() + }) +}) + +async function sendPromptFromCommandBar(page: Page, promptStr: string) { + const commandBarButton = page.getByRole('button', { name: 'Commands' }) + await expect(commandBarButton).toBeVisible() + // Click the command bar button + await commandBarButton.click() + + // Wait for the command bar to appear + const cmdSearchBar = page.getByPlaceholder('Search commands') + await expect(cmdSearchBar).toBeVisible() + + const textToCadCommand = page.getByText('Text-to-CAD') + await expect(textToCadCommand.first()).toBeVisible() + // Click the Text-to-CAD command + await textToCadCommand.first().click() + + // Enter the prompt. + const prompt = page.getByText('Prompt') + await expect(prompt.first()).toBeVisible() + + // Type the prompt. + await page.keyboard.type(promptStr) + await page.waitForTimeout(1000) + await page.keyboard.press('Enter') +} diff --git a/package.json b/package.json index 7523b1256..a21e28662 100644 --- a/package.json +++ b/package.json @@ -17,7 +17,7 @@ "@fortawesome/react-fontawesome": "^0.2.0", "@headlessui/react": "^1.7.19", "@headlessui/tailwindcss": "^0.2.0", - "@kittycad/lib": "^0.0.70", + "@kittycad/lib": "^0.0.76", "@lezer/highlight": "^1.2.0", "@lezer/lr": "^1.4.1", "@react-hook/resize-observer": "^2.0.1", diff --git a/src/components/CommandBar/CommandBarArgument.tsx b/src/components/CommandBar/CommandBarArgument.tsx index 3d66884ee..9bf793ecb 100644 --- a/src/components/CommandBar/CommandBarArgument.tsx +++ b/src/components/CommandBar/CommandBarArgument.tsx @@ -5,6 +5,7 @@ import { CommandArgument } from 'lib/commandTypes' import { useCommandsContext } from 'hooks/useCommandsContext' import CommandBarHeader from './CommandBarHeader' import CommandBarKclInput from './CommandBarKclInput' +import CommandBarTextareaInput from './CommandBarTextareaInput' function CommandBarArgument({ stepBack }: { stepBack: () => void }) { const { commandBarState, commandBarSend } = useCommandsContext() @@ -87,6 +88,14 @@ function ArgumentInput({ return ( ) + case 'text': + return ( + + ) default: return ( & { + inputType: 'text' + name: string + } + stepBack: () => void + onSubmit: (event: unknown) => void +}) { + const { commandBarSend, commandBarState } = useCommandsContext() + useHotkeys('mod + k, mod + /', () => commandBarSend({ type: 'Close' })) + const formRef = useRef(null) + const inputRef = useRef(null) + useTextareaAutoGrow(inputRef) + + useEffect(() => { + if (inputRef.current) { + inputRef.current.focus() + inputRef.current.select() + } + }, [arg, inputRef]) + + function handleSubmit(e: React.FormEvent) { + e.preventDefault() + onSubmit(inputRef.current?.value) + } + + return ( +
+