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() {
|
get cmdSearchInput() {
|
||||||
return this.page.getByTestId('cmd-bar-search')
|
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}`
|
`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
|
projectTextName!: Locator
|
||||||
sortByDateBtn!: Locator
|
sortByDateBtn!: Locator
|
||||||
sortByNameBtn!: Locator
|
sortByNameBtn!: Locator
|
||||||
|
appHeader!: Locator
|
||||||
tutorialBtn!: Locator
|
tutorialBtn!: Locator
|
||||||
|
|
||||||
constructor(page: Page) {
|
constructor(page: Page) {
|
||||||
@ -44,6 +45,7 @@ export class HomePageFixture {
|
|||||||
|
|
||||||
this.sortByDateBtn = this.page.getByTestId('home-sort-by-modified')
|
this.sortByDateBtn = this.page.getByTestId('home-sort-by-modified')
|
||||||
this.sortByNameBtn = this.page.getByTestId('home-sort-by-name')
|
this.sortByNameBtn = this.page.getByTestId('home-sort-by-name')
|
||||||
|
this.appHeader = this.page.getByTestId('app-header')
|
||||||
this.tutorialBtn = this.page.getByTestId('home-tutorial-button')
|
this.tutorialBtn = this.page.getByTestId('home-tutorial-button')
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -125,4 +127,11 @@ export class HomePageFixture {
|
|||||||
|
|
||||||
await this.createAndGoToProject(name)
|
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 networkToggleConnected!: Locator
|
||||||
public engineConnectionsSpinner!: Locator
|
public engineConnectionsSpinner!: Locator
|
||||||
public startEditSketchBtn!: Locator
|
public startEditSketchBtn!: Locator
|
||||||
|
public appHeader!: Locator
|
||||||
|
|
||||||
constructor(page: Page) {
|
constructor(page: Page) {
|
||||||
this.page = page
|
this.page = page
|
||||||
@ -57,6 +58,7 @@ export class SceneFixture {
|
|||||||
this.startEditSketchBtn = page
|
this.startEditSketchBtn = page
|
||||||
.getByRole('button', { name: 'Start Sketch' })
|
.getByRole('button', { name: 'Start Sketch' })
|
||||||
.or(page.getByRole('button', { name: 'Edit Sketch' }))
|
.or(page.getByRole('button', { name: 'Edit Sketch' }))
|
||||||
|
this.appHeader = this.page.getByTestId('app-header')
|
||||||
}
|
}
|
||||||
private _serialiseScene = async (): Promise<SceneSerialised> => {
|
private _serialiseScene = async (): Promise<SceneSerialised> => {
|
||||||
const camera = await this.getCameraInfo()
|
const camera = await this.getCameraInfo()
|
||||||
@ -280,6 +282,13 @@ export class SceneFixture {
|
|||||||
await expect(buttonToTest).toBeVisible()
|
await expect(buttonToTest).toBeVisible()
|
||||||
await buttonToTest.click()
|
await buttonToTest.click()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isNativeFileMenuCreated = async () => {
|
||||||
|
await expect(this.appHeader).toHaveAttribute(
|
||||||
|
'data-native-file-menu',
|
||||||
|
'true'
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isColourArray(
|
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 { 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 { isErrorWhitelisted } from '@e2e/playwright/lib/console-error-whitelist'
|
||||||
import { TEST_SETTINGS, TEST_SETTINGS_KEY } from '@e2e/playwright/storageStates'
|
import { TEST_SETTINGS, TEST_SETTINGS_KEY } from '@e2e/playwright/storageStates'
|
||||||
import { test } from '@e2e/playwright/zoo-test'
|
import { test } from '@e2e/playwright/zoo-test'
|
||||||
@ -1149,3 +1150,77 @@ export function perProjectSettingsToToml(
|
|||||||
// eslint-disable-next-line no-restricted-syntax
|
// eslint-disable-next-line no-restricted-syntax
|
||||||
return TOML.stringify(settings as any)
|
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 toast from 'react-hot-toast'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import ModalContainer from 'react-modal-promise'
|
import ModalContainer from 'react-modal-promise'
|
||||||
@ -42,6 +42,7 @@ import {
|
|||||||
ONBOARDING_TOAST_ID,
|
ONBOARDING_TOAST_ID,
|
||||||
TutorialRequestToast,
|
TutorialRequestToast,
|
||||||
} from '@src/routes/Onboarding/utils'
|
} from '@src/routes/Onboarding/utils'
|
||||||
|
import { reportRejection } from '@src/lib/trap'
|
||||||
|
|
||||||
// CYCLIC REF
|
// CYCLIC REF
|
||||||
sceneInfra.camControls.engineStreamActor = engineStreamActor
|
sceneInfra.camControls.engineStreamActor = engineStreamActor
|
||||||
@ -52,6 +53,7 @@ maybeWriteToDisk()
|
|||||||
|
|
||||||
export function App() {
|
export function App() {
|
||||||
const { project, file } = useLoaderData() as IndexLoaderData
|
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
|
// Keep a lookout for a URL query string that invokes the 'import file from URL' command
|
||||||
useCreateFileLinkQuery((argDefaultValues) => {
|
useCreateFileLinkQuery((argDefaultValues) => {
|
||||||
@ -145,12 +147,25 @@ export function App() {
|
|||||||
}
|
}
|
||||||
}, [location, settings.app.onboardingStatus, navigate])
|
}, [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 (
|
return (
|
||||||
<div className="relative h-full flex flex-col" ref={ref}>
|
<div className="relative h-full flex flex-col" ref={ref}>
|
||||||
<AppHeader
|
<AppHeader
|
||||||
className="transition-opacity transition-duration-75"
|
className="transition-opacity transition-duration-75"
|
||||||
project={{ project, file }}
|
project={{ project, file }}
|
||||||
enableMenu={true}
|
enableMenu={true}
|
||||||
|
nativeFileMenuCreated={nativeFileMenuCreated}
|
||||||
>
|
>
|
||||||
<CommandBarOpenButton />
|
<CommandBarOpenButton />
|
||||||
<ShareButton />
|
<ShareButton />
|
||||||
|
@ -14,6 +14,7 @@ interface AppHeaderProps extends React.PropsWithChildren {
|
|||||||
className?: string
|
className?: string
|
||||||
enableMenu?: boolean
|
enableMenu?: boolean
|
||||||
style?: React.CSSProperties
|
style?: React.CSSProperties
|
||||||
|
nativeFileMenuCreated: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AppHeader = ({
|
export const AppHeader = ({
|
||||||
@ -23,12 +24,14 @@ export const AppHeader = ({
|
|||||||
className = '',
|
className = '',
|
||||||
style,
|
style,
|
||||||
enableMenu = false,
|
enableMenu = false,
|
||||||
|
nativeFileMenuCreated,
|
||||||
}: AppHeaderProps) => {
|
}: AppHeaderProps) => {
|
||||||
const user = useUser()
|
const user = useUser()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<header
|
<header
|
||||||
id="app-header"
|
id="app-header"
|
||||||
|
data-testid="app-header"
|
||||||
className={
|
className={
|
||||||
'w-full grid ' +
|
'w-full grid ' +
|
||||||
styles.header +
|
styles.header +
|
||||||
@ -37,6 +40,7 @@ export const AppHeader = ({
|
|||||||
}overlaid-panes sticky top-0 z-20 px-2 items-start ` +
|
}overlaid-panes sticky top-0 z-20 px-2 items-start ` +
|
||||||
className
|
className
|
||||||
}
|
}
|
||||||
|
data-native-file-menu={nativeFileMenuCreated}
|
||||||
style={style}
|
style={style}
|
||||||
>
|
>
|
||||||
<ProjectSidebarMenu
|
<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">
|
<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
|
<button
|
||||||
|
data-testid="command-bar-close-button"
|
||||||
onClick={() => commandBarActor.send({ type: 'Close' })}
|
onClick={() => commandBarActor.send({ type: 'Close' })}
|
||||||
className="group m-0 p-0 border-none bg-transparent hover:bg-transparent"
|
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 { BROWSER_PATH, PATHS } from '@src/lib/paths'
|
||||||
import { markOnce } from '@src/lib/performance'
|
import { markOnce } from '@src/lib/performance'
|
||||||
import { codeManager, kclManager } from '@src/lib/singletons'
|
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 { type IndexLoaderData } from '@src/lib/types'
|
||||||
import { useSettings, useToken } from '@src/lib/singletons'
|
import { useSettings, useToken } from '@src/lib/singletons'
|
||||||
import { commandBarActor } from '@src/lib/singletons'
|
import { commandBarActor } from '@src/lib/singletons'
|
||||||
@ -59,12 +59,6 @@ export const FileMachineProvider = ({
|
|||||||
const { project, file } = projectData
|
const { project, file } = projectData
|
||||||
|
|
||||||
const filePath = useAbsoluteFilePath()
|
const filePath = useAbsoluteFilePath()
|
||||||
// Only create the native file menus on desktop
|
|
||||||
useEffect(() => {
|
|
||||||
if (isDesktop()) {
|
|
||||||
window.electron.createModelingPageMenu().catch(reportRejection)
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const {
|
const {
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import type { FormEvent, HTMLProps } from 'react'
|
import type { FormEvent, HTMLProps } from 'react'
|
||||||
import { useEffect } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { toast } from 'react-hot-toast'
|
import { toast } from 'react-hot-toast'
|
||||||
import { useHotkeys } from 'react-hotkeys-hook'
|
import { useHotkeys } from 'react-hotkeys-hook'
|
||||||
import {
|
import {
|
||||||
@ -70,13 +70,20 @@ type ReadWriteProjectState = {
|
|||||||
// This route only opens in the desktop context for now,
|
// This route only opens in the desktop context for now,
|
||||||
// as defined in Router.tsx, so we can use the desktop APIs and types.
|
// as defined in Router.tsx, so we can use the desktop APIs and types.
|
||||||
const Home = () => {
|
const Home = () => {
|
||||||
|
const navigate = useNavigate()
|
||||||
const readWriteProjectDir = useCanReadWriteProjectDirectory()
|
const readWriteProjectDir = useCanReadWriteProjectDirectory()
|
||||||
|
const [nativeFileMenuCreated, setNativeFileMenuCreated] = useState(false)
|
||||||
const apiToken = useToken()
|
const apiToken = useToken()
|
||||||
|
|
||||||
// Only create the native file menus on desktop
|
// Only create the native file menus on desktop
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isDesktop()) {
|
if (isDesktop()) {
|
||||||
window.electron.createHomePageMenu().catch(reportRejection)
|
window.electron
|
||||||
|
.createHomePageMenu()
|
||||||
|
.then(() => {
|
||||||
|
setNativeFileMenuCreated(true)
|
||||||
|
})
|
||||||
|
.catch(reportRejection)
|
||||||
}
|
}
|
||||||
billingActor.send({ type: BillingTransition.Update, apiToken })
|
billingActor.send({ type: BillingTransition.Update, apiToken })
|
||||||
}, [])
|
}, [])
|
||||||
@ -94,7 +101,6 @@ const Home = () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const navigate = useNavigate()
|
|
||||||
const settings = useSettings()
|
const settings = useSettings()
|
||||||
const onboardingStatus = settings.app.onboardingStatus.current
|
const onboardingStatus = settings.app.onboardingStatus.current
|
||||||
|
|
||||||
@ -217,7 +223,10 @@ const Home = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative flex flex-col items-stretch h-screen w-screen overflow-hidden">
|
<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">
|
<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
|
<HomeHeader
|
||||||
setQuery={setQuery}
|
setQuery={setQuery}
|
||||||
|
@ -32,13 +32,16 @@ export const Settings = () => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
console.log('hash', location.hash)
|
console.log('hash', location.hash)
|
||||||
if (location.hash) {
|
if (location.hash) {
|
||||||
const element = document.getElementById(location.hash.slice(1))
|
setTimeout(() => {
|
||||||
if (element) {
|
// GOTCHA: Next tick required, you can instantly navigate to a path and this code will find a null element and not scroll into view.
|
||||||
element.scrollIntoView({ block: 'center', behavior: 'smooth' })
|
const element = document.getElementById(location.hash.slice(1))
|
||||||
;(
|
if (element) {
|
||||||
element.querySelector('input, select, textarea') as HTMLInputElement
|
element.scrollIntoView({ block: 'center', behavior: 'smooth' })
|
||||||
)?.focus()
|
;(
|
||||||
}
|
element.querySelector('input, select, textarea') as HTMLInputElement
|
||||||
|
)?.focus()
|
||||||
|
}
|
||||||
|
}, 0)
|
||||||
}
|
}
|
||||||
}, [location.hash])
|
}, [location.hash])
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user