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 highlightedHeaderArg: string commandName: string } | { stage: 'review' headerArguments: Record 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 => { 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) } /** The method will use buttons OR press enter randomly to progress the cmdbar, * this could have unexpected results depending on what's focused * * TODO: This method assumes the user has a valid input to the current stage, * and assumes we are past the `pickCommand` step. */ progressCmdBar = async (shouldFuzzProgressMethod = true) => { await this.page.waitForTimeout(2000) const arrowButton = this.page.getByRole('button', { name: 'arrow right Continue', }) if (await arrowButton.isVisible()) { await arrowButton.click() } else { await this.page .getByRole('button', { name: 'checkmark Submit command' }) .click() } } // 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.page.getByText( 'Use Zoo AI to edit your kcl' ) await expect(promptEditCommand.first()).toBeVisible() await promptEditCommand.first().scrollIntoViewIfNeeded() await promptEditCommand.first().click() } } get cmdSearchInput() { return this.page.getByTestId('cmd-bar-search') } get argumentInput() { return this.page.getByTestId('cmd-bar-arg-value') } 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[1]) => { return this.page.getByRole('option', options) } /** * 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 { const requestBody = request.postDataJSON() // 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/iteration', requestHandler) console.log( `Monitoring text-to-cad API requests. Output will be saved to: ${outputPath}` ) } }