* WIP: Add bidirectional args to point-and-click Extrude Will eventually close #7495 * Wire up edit flow for symmetric * Show skip true args in header in review phase * Add bidirectionalLength * Make currentArg always part of header * WIP * Add twistAng * Proper optional args line in review * Labels in progress button and option arg section heading * Clean up extrude specific changes * Clean up to separate from #7506 * Clean up to separate from #7506 * More clean up across branches * More UI polish * Remove options bool icon * Fix labels for tests * Upgrade e2e tests to cmdBar fixtures with fixes * More fixes * Fixed up more tests related to sweep behavior change * Fix nodeToEdit not having hidden: true on Shell * Add typecheck * WIP: footer buttons * back to reg width * Clean up * Update snapshots * Update snapshots * Clean up * Fix tests and remove label * Refactor * Fix offset plane test * Add CommandBarDivider * Fix step back * Update snapshots * Add comment * Fix it, thanks bot * Clean up and inline optional heading * Little case tweak * Update src/components/CommandBar/CommandBarReview.tsx Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com> * Rename to CommandBarHeaderFooter * Rename to CommandBarHeaderFooter * Clean things up and fix edit * Add test * Revert something quick * Reorg args to match kcl order * Clean up edit arg retrieval error checks * Lint --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: graphite-app[bot] <96075541+graphite-app[bot]@users.noreply.github.com>
		
			
				
	
	
		
			346 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
			
		
		
	
	
			346 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			TypeScript
		
	
	
	
	
	
import * as fs from 'fs'
 | 
						|
import * as path from 'path'
 | 
						|
import type { Locator, Page, Request, Route, TestInfo } from '@playwright/test'
 | 
						|
import { expect } from '@playwright/test'
 | 
						|
 | 
						|
export type CmdBarSerialised =
 | 
						|
  | {
 | 
						|
      stage: 'commandBarClosed'
 | 
						|
    }
 | 
						|
  | {
 | 
						|
      stage: 'pickCommand'
 | 
						|
      // TODO this will need more properties when implemented in _serialiseCmdBar
 | 
						|
    }
 | 
						|
  | {
 | 
						|
      stage: 'arguments'
 | 
						|
      currentArgKey: string
 | 
						|
      currentArgValue: string
 | 
						|
      headerArguments: Record<string, string>
 | 
						|
      highlightedHeaderArg: string
 | 
						|
      commandName: string
 | 
						|
    }
 | 
						|
  | {
 | 
						|
      stage: 'review'
 | 
						|
      headerArguments: Record<string, string>
 | 
						|
      commandName: string
 | 
						|
    }
 | 
						|
 | 
						|
export class CmdBarFixture {
 | 
						|
  public page: Page
 | 
						|
  public cmdBarOpenBtn!: Locator
 | 
						|
  public cmdBarElement!: Locator
 | 
						|
 | 
						|
  constructor(page: Page) {
 | 
						|
    this.page = page
 | 
						|
    this.cmdBarOpenBtn = this.page.getByTestId('command-bar-open-button')
 | 
						|
    this.cmdBarElement = this.page.getByTestId('command-bar')
 | 
						|
  }
 | 
						|
 | 
						|
  get currentArgumentInput() {
 | 
						|
    return this.page.getByTestId('cmd-bar-arg-value')
 | 
						|
  }
 | 
						|
 | 
						|
  private _serialiseCmdBar = async (): Promise<CmdBarSerialised> => {
 | 
						|
    if (!(await this.page.getByTestId('command-bar-wrapper').isVisible())) {
 | 
						|
      return { stage: 'commandBarClosed' }
 | 
						|
    } else if (await this.page.getByTestId('cmd-bar-search').isVisible()) {
 | 
						|
      return { stage: 'pickCommand' }
 | 
						|
    }
 | 
						|
    const reviewForm = this.page.locator('#review-form')
 | 
						|
    const getHeaderArgs = async () => {
 | 
						|
      const inputs = await this.page.getByTestId('cmd-bar-input-tab').all()
 | 
						|
      const entries = await Promise.all(
 | 
						|
        inputs.map((input) => {
 | 
						|
          const key = input
 | 
						|
            .locator('[data-test-name="arg-name"]')
 | 
						|
            .innerText()
 | 
						|
            .then((a) => a.trim())
 | 
						|
          const value = input
 | 
						|
            .getByTestId('header-arg-value')
 | 
						|
            .innerText()
 | 
						|
            .then((a) => a.trim())
 | 
						|
          return Promise.all([key, value])
 | 
						|
        })
 | 
						|
      )
 | 
						|
      return Object.fromEntries(entries)
 | 
						|
    }
 | 
						|
    const getCommandName = () =>
 | 
						|
      this.page.getByTestId('command-name').textContent()
 | 
						|
    if (await reviewForm.isVisible()) {
 | 
						|
      const [headerArguments, commandName] = await Promise.all([
 | 
						|
        getHeaderArgs(),
 | 
						|
        getCommandName(),
 | 
						|
      ])
 | 
						|
      return {
 | 
						|
        stage: 'review',
 | 
						|
        headerArguments,
 | 
						|
        commandName: commandName || '',
 | 
						|
      }
 | 
						|
    }
 | 
						|
    const [
 | 
						|
      currentArgKey,
 | 
						|
      currentArgValue,
 | 
						|
      headerArguments,
 | 
						|
      highlightedHeaderArg,
 | 
						|
      commandName,
 | 
						|
    ] = await Promise.all([
 | 
						|
      this.page.getByTestId('cmd-bar-arg-name').textContent(),
 | 
						|
      this.page.getByTestId('cmd-bar-arg-value').textContent(),
 | 
						|
      getHeaderArgs(),
 | 
						|
      this.page
 | 
						|
        .locator('[data-is-current-arg="true"]')
 | 
						|
        .locator('[data-test-name="arg-name"]')
 | 
						|
        .textContent(),
 | 
						|
      getCommandName(),
 | 
						|
    ])
 | 
						|
    return {
 | 
						|
      stage: 'arguments',
 | 
						|
      currentArgKey: currentArgKey || '',
 | 
						|
      currentArgValue: currentArgValue || '',
 | 
						|
      headerArguments,
 | 
						|
      highlightedHeaderArg: highlightedHeaderArg || '',
 | 
						|
      commandName: commandName || '',
 | 
						|
    }
 | 
						|
  }
 | 
						|
  expectState = async (expected: CmdBarSerialised) => {
 | 
						|
    return expect.poll(() => this._serialiseCmdBar()).toEqual(expected)
 | 
						|
  }
 | 
						|
  /**
 | 
						|
   * This method is used to progress the command bar to the next step, defaulting to clicking the next button.
 | 
						|
   * Optionally, with the `shouldUseKeyboard` parameter, it will hit `Enter` to progress.
 | 
						|
   * * TODO: This method assumes the user has a valid input to the current stage,
 | 
						|
   * and assumes we are past the `pickCommand` step.
 | 
						|
   */
 | 
						|
  progressCmdBar = async (shouldUseKeyboard = false) => {
 | 
						|
    await this.page.waitForTimeout(2000)
 | 
						|
    if (shouldUseKeyboard) {
 | 
						|
      await this.page.keyboard.press('Enter')
 | 
						|
      return
 | 
						|
    }
 | 
						|
 | 
						|
    const arrowButton = this.page.getByTestId('command-bar-continue')
 | 
						|
    if (await arrowButton.isVisible()) {
 | 
						|
      await this.continue()
 | 
						|
    } else {
 | 
						|
      await this.submit()
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  // Added data-testid to the command bar buttons
 | 
						|
  // command-bar-continue are the buttons to go to the next step
 | 
						|
  // does not include the submit which is the final button press
 | 
						|
  // aka the right arrow button
 | 
						|
  continue = async () => {
 | 
						|
    const continueButton = this.page.getByTestId('command-bar-continue')
 | 
						|
    await continueButton.click()
 | 
						|
  }
 | 
						|
 | 
						|
  // Added data-testid to the command bar buttons
 | 
						|
  // command-bar-submit is the button for the final step to submit
 | 
						|
  // the command bar flow aka the checkmark button.
 | 
						|
  submit = async () => {
 | 
						|
    const submitButton = this.page.getByTestId('command-bar-submit')
 | 
						|
    await submitButton.click()
 | 
						|
  }
 | 
						|
 | 
						|
  openCmdBar = async (selectCmd?: 'promptToEdit') => {
 | 
						|
    await this.cmdBarOpenBtn.click()
 | 
						|
    await expect(this.page.getByPlaceholder('Search commands')).toBeVisible()
 | 
						|
    if (selectCmd === 'promptToEdit') {
 | 
						|
      const promptEditCommand = this.selectOption({ name: 'Text-to-CAD Edit' })
 | 
						|
      await expect(promptEditCommand.first()).toBeVisible()
 | 
						|
      await promptEditCommand.first().scrollIntoViewIfNeeded()
 | 
						|
      await promptEditCommand.first().click()
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  closeCmdBar = async () => {
 | 
						|
    const cmdBarCloseBtn = this.page.getByTestId('command-bar-close-button')
 | 
						|
    await cmdBarCloseBtn.click()
 | 
						|
    await expect(this.cmdBarElement).not.toBeVisible()
 | 
						|
  }
 | 
						|
 | 
						|
  get cmdSearchInput() {
 | 
						|
    return this.page.getByTestId('cmd-bar-search')
 | 
						|
  }
 | 
						|
 | 
						|
  get argumentInput() {
 | 
						|
    return this.page.getByTestId('cmd-bar-arg-value')
 | 
						|
  }
 | 
						|
 | 
						|
  get variableCheckbox() {
 | 
						|
    return this.page.getByTestId('cmd-bar-variable-checkbox')
 | 
						|
  }
 | 
						|
 | 
						|
  get cmdOptions() {
 | 
						|
    return this.page.getByTestId('cmd-bar-option')
 | 
						|
  }
 | 
						|
 | 
						|
  chooseCommand = async (commandName: string) => {
 | 
						|
    await this.cmdOptions.getByText(commandName).click()
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Select an option from the command bar
 | 
						|
   */
 | 
						|
  selectOption = (options: Parameters<typeof this.page.getByRole>[1]) => {
 | 
						|
    return this.page.getByRole('option', options)
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Select an optional argument from the command bar during review
 | 
						|
   */
 | 
						|
  clickOptionalArgument = async (argName: string) => {
 | 
						|
    await this.page.getByTestId(`cmd-bar-add-optional-arg-${argName}`).click()
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Clicks the Create new variable button for kcl input
 | 
						|
   */
 | 
						|
  createNewVariable = async () => {
 | 
						|
    await this.variableCheckbox.click()
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * Captures a snapshot of the request sent to the text-to-cad API endpoint
 | 
						|
   * and saves it to a file named after the current test.
 | 
						|
   *
 | 
						|
   * The snapshot file will be saved in the specified directory with a filename
 | 
						|
   * derived from the test's full path (including describe blocks).
 | 
						|
   *
 | 
						|
   * @param testInfoInOrderToGetTestTitle The TestInfo object from the test context
 | 
						|
   * @param customOutputDir Optional custom directory for the output file
 | 
						|
   */
 | 
						|
  async captureTextToCadRequestSnapshot(
 | 
						|
    testInfoInOrderToGetTestTitle: TestInfo,
 | 
						|
    customOutputDir = 'e2e/playwright/snapshots/prompt-to-edit'
 | 
						|
  ) {
 | 
						|
    // First sanitize each title component individually
 | 
						|
    const sanitizedTitleComponents = [
 | 
						|
      ...testInfoInOrderToGetTestTitle.titlePath.slice(0, -1), // Get all parent titles
 | 
						|
      testInfoInOrderToGetTestTitle.title, // Add the test title
 | 
						|
    ].map(
 | 
						|
      (component) =>
 | 
						|
        component
 | 
						|
          .replace(/[^a-z0-9]/gi, '-') // Replace non-alphanumeric chars with hyphens
 | 
						|
          .toLowerCase()
 | 
						|
          .replace(/-+/g, '-') // Replace multiple consecutive hyphens with a single one
 | 
						|
          .replace(/^-|-$/g, '') // Remove leading/trailing hyphens
 | 
						|
    )
 | 
						|
 | 
						|
    // Join the sanitized components with -- as a clear separator
 | 
						|
    const sanitizedTestName = sanitizedTitleComponents.join('--')
 | 
						|
 | 
						|
    // Create the output path
 | 
						|
    const outputPath = path.join(
 | 
						|
      customOutputDir,
 | 
						|
      `${sanitizedTestName}.snap.json`
 | 
						|
    )
 | 
						|
 | 
						|
    // Create a handler function that saves request bodies to a file
 | 
						|
    const requestHandler = (route: Route, request: Request) => {
 | 
						|
      try {
 | 
						|
        // Get the raw post data
 | 
						|
        const postData = request.postData()
 | 
						|
        if (!postData) {
 | 
						|
          console.error('No post data found in request')
 | 
						|
          return
 | 
						|
        }
 | 
						|
 | 
						|
        // Extract all parts from the multipart form data
 | 
						|
        const boundary = postData.match(/------WebKitFormBoundary[^\r\n]*/)?.[0]
 | 
						|
        if (!boundary) {
 | 
						|
          console.error('Could not find form boundary')
 | 
						|
          return
 | 
						|
        }
 | 
						|
 | 
						|
        const parts = postData.split(boundary).filter((part) => part.trim())
 | 
						|
        const files: Record<string, string> = {}
 | 
						|
        let eventData = null
 | 
						|
 | 
						|
        for (const part of parts) {
 | 
						|
          // Skip the final boundary marker
 | 
						|
          if (part.startsWith('--')) continue
 | 
						|
 | 
						|
          const nameMatch = part.match(/name="([^"]+)"/)
 | 
						|
          if (!nameMatch) continue
 | 
						|
 | 
						|
          const name = nameMatch[1]
 | 
						|
          const content = part.split(/\r?\n\r?\n/)[1]?.trim()
 | 
						|
          if (!content) continue
 | 
						|
 | 
						|
          if (name === 'event') {
 | 
						|
            eventData = JSON.parse(content)
 | 
						|
          } else {
 | 
						|
            files[name] = content
 | 
						|
          }
 | 
						|
        }
 | 
						|
 | 
						|
        if (!eventData) {
 | 
						|
          console.error('Could not find event JSON in multipart form data')
 | 
						|
          return
 | 
						|
        }
 | 
						|
 | 
						|
        const requestBody = {
 | 
						|
          ...eventData,
 | 
						|
          files,
 | 
						|
        }
 | 
						|
 | 
						|
        // Ensure directory exists
 | 
						|
        const dir = path.dirname(outputPath)
 | 
						|
        if (!fs.existsSync(dir)) {
 | 
						|
          fs.mkdirSync(dir, { recursive: true })
 | 
						|
        }
 | 
						|
 | 
						|
        // Write the request body to the file
 | 
						|
        fs.writeFileSync(outputPath, JSON.stringify(requestBody, null, 2))
 | 
						|
 | 
						|
        console.log(`Saved text-to-cad API request to: ${outputPath}`)
 | 
						|
      } catch (error) {
 | 
						|
        console.error('Error processing text-to-cad request:', error)
 | 
						|
      }
 | 
						|
 | 
						|
      // Use void to explicitly mark the promise as ignored
 | 
						|
      void route.continue()
 | 
						|
    }
 | 
						|
 | 
						|
    // Start monitoring requests
 | 
						|
    await this.page.route(
 | 
						|
      '**/ml/text-to-cad/multi-file/iteration',
 | 
						|
      requestHandler
 | 
						|
    )
 | 
						|
 | 
						|
    console.log(
 | 
						|
      `Monitoring text-to-cad API requests. Output will be saved to: ${outputPath}`
 | 
						|
    )
 | 
						|
  }
 | 
						|
 | 
						|
  async toBeOpened() {
 | 
						|
    // Check that the command bar is opened
 | 
						|
    await expect(this.cmdBarElement).toBeVisible({ timeout: 10_000 })
 | 
						|
  }
 | 
						|
 | 
						|
  async toBeClosed() {
 | 
						|
    // Check that the command bar is closed
 | 
						|
    await expect(this.cmdBarElement).not.toBeVisible({ timeout: 10_000 })
 | 
						|
  }
 | 
						|
 | 
						|
  async expectArgValue(value: string) {
 | 
						|
    // Check the placeholder project name exists
 | 
						|
    const actualArgument = await this.cmdBarElement
 | 
						|
      .getByTestId('cmd-bar-arg-value')
 | 
						|
      .inputValue()
 | 
						|
    const expectedArgument = value
 | 
						|
    expect(actualArgument).toBe(expectedArgument)
 | 
						|
  }
 | 
						|
 | 
						|
  async expectCommandName(value: string) {
 | 
						|
    // Check the placeholder project name exists
 | 
						|
    const actual = await this.cmdBarElement
 | 
						|
      .getByTestId('command-name')
 | 
						|
      .textContent()
 | 
						|
    const expected = value
 | 
						|
    expect(actual).toBe(expected)
 | 
						|
  }
 | 
						|
}
 |