Files
modeling-app/e2e/playwright/fixtures/cmdBarFixture.ts
Kurt Hutten d9be308c36 ~Maybe~ (Lee: ABSOLUTELY) use biome (#6327)
* maybe use biome

* import organising

* Removed unused new fmt commands; fix to not use linter

* Don't use fmt:generated - fmt is fast, but also use Makefile for this sort of thing

---------

Co-authored-by: lee-at-zoo-corp <lee@zoo.dev>
2025-04-15 10:11:25 -04:00

248 lines
7.9 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)
}
/** 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<typeof this.page.getByRole>[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}`
)
}
}