Restore the native file menu tests (#6279)
* Restore the native file menu tests * fix: saving off progress * chore: making progress cleaning up these verbose tests and improving app logic for e2e * chore: rewriting tests * fix: reworking application logic for file menu in the scene and e2e scene file menu test * chore: updating more e2e tests * fix: updated all the tests, auto fixers * fix: trying to improve tests within E2E, they aren't failing locally even with --repeat-each=10 * fix: application logic has a bug that you can navigate instantly but the scroll to view code will not trigger which breaks end to end tests * fix: improving E2E tests * fix: fixing clipboard typo * fix: porting test() for each native file menu to a test.step to speed it up * fix: auto fixes and console log helper function for playwright runtimes * fix: more cleanup * fix: trying to fix these... * fix: got the tests working * fix: addressing PR comments * fix: trying to stablize the tests * fix: auto fixes * fix: trying to make it the command name and not arg? could be a source of race condition if the input is not written fast enough? * fix: maybe because this close locator was running too quickly? * fix: panic timeout, classic * fix: these are gone * fix: shorter waits --------- Co-authored-by: Kevin Nadro <kevin@zoo.dev> Co-authored-by: Kevin Nadro <nadr0@users.noreply.github.com> Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
This commit is contained in:
		@ -155,6 +155,12 @@ export class CmdBarFixture {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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')
 | 
			
		||||
  }
 | 
			
		||||
@ -298,4 +304,27 @@ export class CmdBarFixture {
 | 
			
		||||
      `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 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)
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -24,6 +24,7 @@ export class HomePageFixture {
 | 
			
		||||
  projectTextName!: Locator
 | 
			
		||||
  sortByDateBtn!: Locator
 | 
			
		||||
  sortByNameBtn!: Locator
 | 
			
		||||
  appHeader!: Locator
 | 
			
		||||
  tutorialBtn!: Locator
 | 
			
		||||
 | 
			
		||||
  constructor(page: Page) {
 | 
			
		||||
@ -44,6 +45,7 @@ export class HomePageFixture {
 | 
			
		||||
 | 
			
		||||
    this.sortByDateBtn = this.page.getByTestId('home-sort-by-modified')
 | 
			
		||||
    this.sortByNameBtn = this.page.getByTestId('home-sort-by-name')
 | 
			
		||||
    this.appHeader = this.page.getByTestId('app-header')
 | 
			
		||||
    this.tutorialBtn = this.page.getByTestId('home-tutorial-button')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -125,4 +127,11 @@ export class HomePageFixture {
 | 
			
		||||
 | 
			
		||||
    await this.createAndGoToProject(name)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isNativeFileMenuCreated = async () => {
 | 
			
		||||
    await expect(this.appHeader).toHaveAttribute(
 | 
			
		||||
      'data-native-file-menu',
 | 
			
		||||
      'true'
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -46,6 +46,7 @@ export class SceneFixture {
 | 
			
		||||
  public networkToggleConnected!: Locator
 | 
			
		||||
  public engineConnectionsSpinner!: Locator
 | 
			
		||||
  public startEditSketchBtn!: Locator
 | 
			
		||||
  public appHeader!: Locator
 | 
			
		||||
 | 
			
		||||
  constructor(page: Page) {
 | 
			
		||||
    this.page = page
 | 
			
		||||
@ -57,6 +58,7 @@ export class SceneFixture {
 | 
			
		||||
    this.startEditSketchBtn = page
 | 
			
		||||
      .getByRole('button', { name: 'Start Sketch' })
 | 
			
		||||
      .or(page.getByRole('button', { name: 'Edit Sketch' }))
 | 
			
		||||
    this.appHeader = this.page.getByTestId('app-header')
 | 
			
		||||
  }
 | 
			
		||||
  private _serialiseScene = async (): Promise<SceneSerialised> => {
 | 
			
		||||
    const camera = await this.getCameraInfo()
 | 
			
		||||
@ -280,6 +282,13 @@ export class SceneFixture {
 | 
			
		||||
    await expect(buttonToTest).toBeVisible()
 | 
			
		||||
    await buttonToTest.click()
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  isNativeFileMenuCreated = async () => {
 | 
			
		||||
    await expect(this.appHeader).toHaveAttribute(
 | 
			
		||||
      'data-native-file-menu',
 | 
			
		||||
      'true'
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function isColourArray(
 | 
			
		||||
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@ -21,6 +21,7 @@ export const token = process.env.token || ''
 | 
			
		||||
 | 
			
		||||
import type { ProjectConfiguration } from '@rust/kcl-lib/bindings/ProjectConfiguration'
 | 
			
		||||
 | 
			
		||||
import type { ElectronZoo } from '@e2e/playwright/fixtures/fixtureSetup'
 | 
			
		||||
import { isErrorWhitelisted } from '@e2e/playwright/lib/console-error-whitelist'
 | 
			
		||||
import { TEST_SETTINGS, TEST_SETTINGS_KEY } from '@e2e/playwright/storageStates'
 | 
			
		||||
import { test } from '@e2e/playwright/zoo-test'
 | 
			
		||||
@ -1149,3 +1150,77 @@ export function perProjectSettingsToToml(
 | 
			
		||||
  // eslint-disable-next-line no-restricted-syntax
 | 
			
		||||
  return TOML.stringify(settings as any)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function clickElectronNativeMenuById(
 | 
			
		||||
  tronApp: ElectronZoo,
 | 
			
		||||
  menuId: string
 | 
			
		||||
) {
 | 
			
		||||
  const clickWasTriggered = await tronApp.electron.evaluate(
 | 
			
		||||
    async ({ app }, menuId) => {
 | 
			
		||||
      if (!app || !app.applicationMenu) {
 | 
			
		||||
        return false
 | 
			
		||||
      }
 | 
			
		||||
      const menu = app.applicationMenu.getMenuItemById(menuId)
 | 
			
		||||
      if (!menu) return false
 | 
			
		||||
      menu.click()
 | 
			
		||||
      return true
 | 
			
		||||
    },
 | 
			
		||||
    menuId
 | 
			
		||||
  )
 | 
			
		||||
  expect(clickWasTriggered).toBe(true)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function findElectronNativeMenuById(
 | 
			
		||||
  tronApp: ElectronZoo,
 | 
			
		||||
  menuId: string
 | 
			
		||||
) {
 | 
			
		||||
  const found = await tronApp.electron.evaluate(async ({ app }, menuId) => {
 | 
			
		||||
    if (!app || !app.applicationMenu) {
 | 
			
		||||
      return false
 | 
			
		||||
    }
 | 
			
		||||
    const menu = app.applicationMenu.getMenuItemById(menuId)
 | 
			
		||||
    if (!menu) return false
 | 
			
		||||
    return true
 | 
			
		||||
  }, menuId)
 | 
			
		||||
  expect(found).toBe(true)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function openSettingsExpectText(page: Page, text: string) {
 | 
			
		||||
  const settings = page.getByTestId('settings-dialog-panel')
 | 
			
		||||
  await expect(settings).toBeVisible()
 | 
			
		||||
  // You are viewing the user tab
 | 
			
		||||
  const actualText = settings.getByText(text)
 | 
			
		||||
  await expect(actualText).toBeVisible()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export async function openSettingsExpectLocator(page: Page, selector: string) {
 | 
			
		||||
  const settings = page.getByTestId('settings-dialog-panel')
 | 
			
		||||
  await expect(settings).toBeVisible()
 | 
			
		||||
  // You are viewing the keybindings tab
 | 
			
		||||
  const settingsLocator = settings.locator(selector)
 | 
			
		||||
  await expect(settingsLocator).toBeVisible()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * A developer helper function to make playwright send all the console logs to stdout
 | 
			
		||||
 * Call this within your E2E test and pass in the page or the tronApp to get as many
 | 
			
		||||
 * logs piped to stdout for debugging
 | 
			
		||||
 */
 | 
			
		||||
export async function enableConsoleLogEverything({
 | 
			
		||||
  page,
 | 
			
		||||
  tronApp,
 | 
			
		||||
}: { page?: Page; tronApp?: ElectronZoo }) {
 | 
			
		||||
  page?.on('console', (msg) => {
 | 
			
		||||
    console.log(`[Page-log]: ${msg.text()}`)
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  tronApp?.electron.on('window', async (electronPage) => {
 | 
			
		||||
    electronPage.on('console', (msg) => {
 | 
			
		||||
      console.log(`[Renderer] ${msg.type()}: ${msg.text()}`)
 | 
			
		||||
    })
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  tronApp?.electron.on('console', (msg) => {
 | 
			
		||||
    console.log(`[Main] ${msg.type()}: ${msg.text()}`)
 | 
			
		||||
  })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
							
								
								
									
										17
									
								
								src/App.tsx
									
									
									
									
									
								
							
							
						
						
									
										17
									
								
								src/App.tsx
									
									
									
									
									
								
							@ -1,4 +1,4 @@
 | 
			
		||||
import { useEffect, useRef } from 'react'
 | 
			
		||||
import { useEffect, useRef, useState } from 'react'
 | 
			
		||||
import toast from 'react-hot-toast'
 | 
			
		||||
import { useHotkeys } from 'react-hotkeys-hook'
 | 
			
		||||
import ModalContainer from 'react-modal-promise'
 | 
			
		||||
@ -42,6 +42,7 @@ import {
 | 
			
		||||
  ONBOARDING_TOAST_ID,
 | 
			
		||||
  TutorialRequestToast,
 | 
			
		||||
} from '@src/routes/Onboarding/utils'
 | 
			
		||||
import { reportRejection } from '@src/lib/trap'
 | 
			
		||||
 | 
			
		||||
// CYCLIC REF
 | 
			
		||||
sceneInfra.camControls.engineStreamActor = engineStreamActor
 | 
			
		||||
@ -52,6 +53,7 @@ maybeWriteToDisk()
 | 
			
		||||
 | 
			
		||||
export function App() {
 | 
			
		||||
  const { project, file } = useLoaderData() as IndexLoaderData
 | 
			
		||||
  const [nativeFileMenuCreated, setNativeFileMenuCreated] = useState(false)
 | 
			
		||||
 | 
			
		||||
  // Keep a lookout for a URL query string that invokes the 'import file from URL' command
 | 
			
		||||
  useCreateFileLinkQuery((argDefaultValues) => {
 | 
			
		||||
@ -145,12 +147,25 @@ export function App() {
 | 
			
		||||
    }
 | 
			
		||||
  }, [location, settings.app.onboardingStatus, navigate])
 | 
			
		||||
 | 
			
		||||
  // Only create the native file menus on desktop
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (isDesktop()) {
 | 
			
		||||
      window.electron
 | 
			
		||||
        .createModelingPageMenu()
 | 
			
		||||
        .then(() => {
 | 
			
		||||
          setNativeFileMenuCreated(true)
 | 
			
		||||
        })
 | 
			
		||||
        .catch(reportRejection)
 | 
			
		||||
    }
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="relative h-full flex flex-col" ref={ref}>
 | 
			
		||||
      <AppHeader
 | 
			
		||||
        className="transition-opacity transition-duration-75"
 | 
			
		||||
        project={{ project, file }}
 | 
			
		||||
        enableMenu={true}
 | 
			
		||||
        nativeFileMenuCreated={nativeFileMenuCreated}
 | 
			
		||||
      >
 | 
			
		||||
        <CommandBarOpenButton />
 | 
			
		||||
        <ShareButton />
 | 
			
		||||
 | 
			
		||||
@ -14,6 +14,7 @@ interface AppHeaderProps extends React.PropsWithChildren {
 | 
			
		||||
  className?: string
 | 
			
		||||
  enableMenu?: boolean
 | 
			
		||||
  style?: React.CSSProperties
 | 
			
		||||
  nativeFileMenuCreated: boolean
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const AppHeader = ({
 | 
			
		||||
@ -23,12 +24,14 @@ export const AppHeader = ({
 | 
			
		||||
  className = '',
 | 
			
		||||
  style,
 | 
			
		||||
  enableMenu = false,
 | 
			
		||||
  nativeFileMenuCreated,
 | 
			
		||||
}: AppHeaderProps) => {
 | 
			
		||||
  const user = useUser()
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <header
 | 
			
		||||
      id="app-header"
 | 
			
		||||
      data-testid="app-header"
 | 
			
		||||
      className={
 | 
			
		||||
        'w-full grid ' +
 | 
			
		||||
        styles.header +
 | 
			
		||||
@ -37,6 +40,7 @@ export const AppHeader = ({
 | 
			
		||||
        }overlaid-panes sticky top-0 z-20 px-2 items-start ` +
 | 
			
		||||
        className
 | 
			
		||||
      }
 | 
			
		||||
      data-native-file-menu={nativeFileMenuCreated}
 | 
			
		||||
      style={style}
 | 
			
		||||
    >
 | 
			
		||||
      <ProjectSidebarMenu
 | 
			
		||||
 | 
			
		||||
@ -169,6 +169,7 @@ export const CommandBar = () => {
 | 
			
		||||
            )}
 | 
			
		||||
            <div className="flex flex-col gap-2 !absolute left-auto right-full top-[-3px] m-2.5 p-0 border-none bg-transparent hover:bg-transparent">
 | 
			
		||||
              <button
 | 
			
		||||
                data-testid="command-bar-close-button"
 | 
			
		||||
                onClick={() => commandBarActor.send({ type: 'Close' })}
 | 
			
		||||
                className="group m-0 p-0 border-none bg-transparent hover:bg-transparent"
 | 
			
		||||
              >
 | 
			
		||||
 | 
			
		||||
@ -29,7 +29,7 @@ import { kclCommands } from '@src/lib/kclCommands'
 | 
			
		||||
import { BROWSER_PATH, PATHS } from '@src/lib/paths'
 | 
			
		||||
import { markOnce } from '@src/lib/performance'
 | 
			
		||||
import { codeManager, kclManager } from '@src/lib/singletons'
 | 
			
		||||
import { err, reportRejection } from '@src/lib/trap'
 | 
			
		||||
import { err } from '@src/lib/trap'
 | 
			
		||||
import { type IndexLoaderData } from '@src/lib/types'
 | 
			
		||||
import { useSettings, useToken } from '@src/lib/singletons'
 | 
			
		||||
import { commandBarActor } from '@src/lib/singletons'
 | 
			
		||||
@ -59,12 +59,6 @@ export const FileMachineProvider = ({
 | 
			
		||||
  const { project, file } = projectData
 | 
			
		||||
 | 
			
		||||
  const filePath = useAbsoluteFilePath()
 | 
			
		||||
  // Only create the native file menus on desktop
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (isDesktop()) {
 | 
			
		||||
      window.electron.createModelingPageMenu().catch(reportRejection)
 | 
			
		||||
    }
 | 
			
		||||
  }, [])
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    const {
 | 
			
		||||
 | 
			
		||||
@ -1,5 +1,5 @@
 | 
			
		||||
import type { FormEvent, HTMLProps } from 'react'
 | 
			
		||||
import { useEffect } from 'react'
 | 
			
		||||
import { useEffect, useState } from 'react'
 | 
			
		||||
import { toast } from 'react-hot-toast'
 | 
			
		||||
import { useHotkeys } from 'react-hotkeys-hook'
 | 
			
		||||
import {
 | 
			
		||||
@ -70,13 +70,20 @@ type ReadWriteProjectState = {
 | 
			
		||||
// This route only opens in the desktop context for now,
 | 
			
		||||
// as defined in Router.tsx, so we can use the desktop APIs and types.
 | 
			
		||||
const Home = () => {
 | 
			
		||||
  const navigate = useNavigate()
 | 
			
		||||
  const readWriteProjectDir = useCanReadWriteProjectDirectory()
 | 
			
		||||
  const [nativeFileMenuCreated, setNativeFileMenuCreated] = useState(false)
 | 
			
		||||
  const apiToken = useToken()
 | 
			
		||||
 | 
			
		||||
  // Only create the native file menus on desktop
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (isDesktop()) {
 | 
			
		||||
      window.electron.createHomePageMenu().catch(reportRejection)
 | 
			
		||||
      window.electron
 | 
			
		||||
        .createHomePageMenu()
 | 
			
		||||
        .then(() => {
 | 
			
		||||
          setNativeFileMenuCreated(true)
 | 
			
		||||
        })
 | 
			
		||||
        .catch(reportRejection)
 | 
			
		||||
    }
 | 
			
		||||
    billingActor.send({ type: BillingTransition.Update, apiToken })
 | 
			
		||||
  }, [])
 | 
			
		||||
@ -94,7 +101,6 @@ const Home = () => {
 | 
			
		||||
  })
 | 
			
		||||
 | 
			
		||||
  const location = useLocation()
 | 
			
		||||
  const navigate = useNavigate()
 | 
			
		||||
  const settings = useSettings()
 | 
			
		||||
  const onboardingStatus = settings.app.onboardingStatus.current
 | 
			
		||||
 | 
			
		||||
@ -217,7 +223,10 @@ const Home = () => {
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="relative flex flex-col items-stretch h-screen w-screen overflow-hidden">
 | 
			
		||||
      <AppHeader showToolbar={false} />
 | 
			
		||||
      <AppHeader
 | 
			
		||||
        nativeFileMenuCreated={nativeFileMenuCreated}
 | 
			
		||||
        showToolbar={false}
 | 
			
		||||
      />
 | 
			
		||||
      <div className="overflow-hidden self-stretch w-full flex-1 home-layout max-w-4xl lg:max-w-5xl xl:max-w-7xl mb-12 px-4 mx-auto mt-8 lg:mt-24 lg:px-0">
 | 
			
		||||
        <HomeHeader
 | 
			
		||||
          setQuery={setQuery}
 | 
			
		||||
 | 
			
		||||
@ -32,13 +32,16 @@ export const Settings = () => {
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    console.log('hash', location.hash)
 | 
			
		||||
    if (location.hash) {
 | 
			
		||||
      const element = document.getElementById(location.hash.slice(1))
 | 
			
		||||
      if (element) {
 | 
			
		||||
        element.scrollIntoView({ block: 'center', behavior: 'smooth' })
 | 
			
		||||
        ;(
 | 
			
		||||
          element.querySelector('input, select, textarea') as HTMLInputElement
 | 
			
		||||
        )?.focus()
 | 
			
		||||
      }
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        // GOTCHA: Next tick required, you can instantly navigate to a path and this code will find a null element and not scroll into view.
 | 
			
		||||
        const element = document.getElementById(location.hash.slice(1))
 | 
			
		||||
        if (element) {
 | 
			
		||||
          element.scrollIntoView({ block: 'center', behavior: 'smooth' })
 | 
			
		||||
          ;(
 | 
			
		||||
            element.querySelector('input, select, textarea') as HTMLInputElement
 | 
			
		||||
          )?.focus()
 | 
			
		||||
        }
 | 
			
		||||
      }, 0)
 | 
			
		||||
    }
 | 
			
		||||
  }, [location.hash])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
		Reference in New Issue
	
	Block a user