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:
Jace Browning
2025-05-14 09:06:29 -04:00
committed by GitHub
parent 1e487ef3bd
commit edb424988d
11 changed files with 703 additions and 2356 deletions

View File

@ -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)
}
}

View File

@ -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'
)
}
}

View File

@ -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

View File

@ -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()}`)
})
}

View File

@ -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 />

View File

@ -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

View File

@ -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"
>

View File

@ -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 {

View File

@ -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}

View File

@ -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])