Fix playwright mental model, don't make me psycho, thanks (#5680)

* Fix flakey tests with new toolbar.exitSketch

* tsc && lint && fmt

* Disable pw electron thing again

* Unfrig Playwright-Electron a ton; fix another ton of flakes.

* More deflaky

* Fix a ton of tests and playwright related hell

* Run jess's magic incantation to build rust kcl things

* yarn tsc

* yarn lint

* yarn fmt

* Remove double logs

* Revert to old settings spreads momentarily

* Expect error *in the fixtureSetup*, does not circumvent typechecking for regular usage

* Fix unit tests
This commit is contained in:
49fl
2025-03-13 10:54:00 -04:00
committed by GitHub
parent e894242768
commit c441a3ab1c
28 changed files with 903 additions and 972 deletions

View File

@ -1,4 +1,5 @@
import { test, expect, Page } from './zoo-test' import { Page } from '@playwright/test'
import { test, expect } from './zoo-test'
import { import {
getUtils, getUtils,
TEST_COLORS, TEST_COLORS,

View File

@ -1,4 +1,5 @@
import { test, expect, Page } from './zoo-test' import { Page } from '@playwright/test'
import { test, expect } from './zoo-test'
import { HomePageFixture } from './fixtures/homePageFixture' import { HomePageFixture } from './fixtures/homePageFixture'
import { getUtils } from './test-utils' import { getUtils } from './test-utils'
import { EngineCommand } from 'lang/std/artifactGraph' import { EngineCommand } from 'lang/std/artifactGraph'

View File

@ -10,7 +10,11 @@ import fsp from 'fs/promises'
test( test(
'export works on the first try', 'export works on the first try',
{ tag: ['@electron', '@skipLocalEngine'] }, { tag: ['@electron', '@skipLocalEngine'] },
async ({ page, context, scene }, testInfo) => { async ({ page, context, scene, tronApp }, testInfo) => {
if (!tronApp) {
fail()
}
await context.folderSetupFn(async (dir) => { await context.folderSetupFn(async (dir) => {
const bracketDir = path.join(dir, 'bracket') const bracketDir = path.join(dir, 'bracket')
await Promise.all([fsp.mkdir(bracketDir, { recursive: true })]) await Promise.all([fsp.mkdir(bracketDir, { recursive: true })])
@ -86,7 +90,7 @@ test(
await expect(exportingToastMessage).not.toBeVisible() await expect(exportingToastMessage).not.toBeVisible()
const firstFileFullPath = path.resolve( const firstFileFullPath = path.resolve(
getPlaywrightDownloadDir(page), getPlaywrightDownloadDir(tronApp.projectDirName),
exportFileName exportFileName
) )
await test.step('Check the export size', async () => { await test.step('Check the export size', async () => {
@ -165,7 +169,7 @@ test(
])) ]))
const secondFileFullPath = path.resolve( const secondFileFullPath = path.resolve(
getPlaywrightDownloadDir(page), getPlaywrightDownloadDir(tronApp.projectDirName),
exportFileName exportFileName
) )
await test.step('Check the export size', async () => { await test.step('Check the export size', async () => {

View File

@ -158,11 +158,14 @@ test.describe('when using the file tree to', () => {
await createNewFile('lee') await createNewFile('lee')
await test.step('Postcondition: there are 5 new lee-*.kcl files', async () => { await test.step('Postcondition: there are 5 new lee-*.kcl files', async () => {
await expect( await expect
.poll(() =>
page page
.locator('[data-testid="file-pane-scroll-container"] button') .locator('[data-testid="file-pane-scroll-container"] button')
.filter({ hasText: /lee[-]?[0-5]?/ }) .filter({ hasText: /lee[-]?[0-5]?/ })
).toHaveCount(5) .count()
)
.toEqual(5)
}) })
} }
) )

View File

@ -27,28 +27,19 @@ type CmdBarSerialised =
export class CmdBarFixture { export class CmdBarFixture {
public page: Page public page: Page
public cmdBarOpenBtn!: Locator
get cmdBarOpenBtn() { public cmdBarElement!: Locator
return this.page.getByTestId('command-bar-open-button')
}
get cmdBarElement() {
return this.page.getByTestId('command-bar')
}
constructor(page: Page) { constructor(page: Page) {
this.page = page this.page = page
this.cmdBarOpenBtn = this.page.getByTestId('command-bar-open-button')
this.cmdBarElement = this.page.getByTestId('command-bar')
} }
get currentArgumentInput() { get currentArgumentInput() {
return this.page.getByTestId('cmd-bar-arg-value') return this.page.getByTestId('cmd-bar-arg-value')
} }
// Put all selectors here because this method is re-run on fixture creation.
reConstruct = (page: Page) => {
this.page = page
}
private _serialiseCmdBar = async (): Promise<CmdBarSerialised> => { private _serialiseCmdBar = async (): Promise<CmdBarSerialised> => {
if (!(await this.page.getByTestId('command-bar-wrapper').isVisible())) { if (!(await this.page.getByTestId('command-bar-wrapper').isVisible())) {
return { stage: 'commandBarClosed' } return { stage: 'commandBarClosed' }

View File

@ -24,11 +24,6 @@ export class EditorFixture {
constructor(page: Page) { constructor(page: Page) {
this.page = page this.page = page
this.reConstruct(page)
}
reConstruct = (page: Page) => {
this.page = page
this.codeContent = page.locator('.cm-content[data-language="kcl"]') this.codeContent = page.locator('.cm-content[data-language="kcl"]')
this.diagnosticsTooltip = page.locator('.cm-tooltip-lint') this.diagnosticsTooltip = page.locator('.cm-tooltip-lint')
this.diagnosticsGutterIcon = page.locator('.cm-lint-marker-error') this.diagnosticsGutterIcon = page.locator('.cm-lint-marker-error')

View File

@ -1,13 +1,31 @@
/* eslint-disable react-hooks/rules-of-hooks */
import type { import type {
BrowserContext, BrowserContext,
ElectronApplication, ElectronApplication,
Fixtures as PlaywrightFixtures,
TestInfo, TestInfo,
Page, Page,
} from '@playwright/test' } from '@playwright/test'
import { getUtils, setup, setupElectron } from '../test-utils' import {
_electron as electron,
PlaywrightTestArgs,
PlaywrightWorkerArgs,
} from '@playwright/test'
import * as TOML from '@iarna/toml'
import {
TEST_SETTINGS_KEY,
TEST_SETTINGS_CORRUPTED,
TEST_SETTINGS,
TEST_SETTINGS_DEFAULT_THEME,
} from '../storageStates'
import { SETTINGS_FILE_NAME, PROJECT_SETTINGS_FILE_NAME } from 'lib/constants'
import { getUtils, setup } from '../test-utils'
import fsp from 'fs/promises' import fsp from 'fs/promises'
import { join } from 'path' import fs from 'node:fs'
import path from 'path'
import { CmdBarFixture } from './cmdBarFixture' import { CmdBarFixture } from './cmdBarFixture'
import { EditorFixture } from './editorFixture' import { EditorFixture } from './editorFixture'
import { ToolbarFixture } from './toolbarFixture' import { ToolbarFixture } from './toolbarFixture'
@ -23,7 +41,7 @@ export class AuthenticatedApp {
public readonly testInfo: TestInfo public readonly testInfo: TestInfo
public readonly viewPortSize = { width: 1200, height: 500 } public readonly viewPortSize = { width: 1200, height: 500 }
public electronApp: undefined | ElectronApplication public electronApp: undefined | ElectronApplication
public dir: string = '' public projectDirName: string = ''
constructor(context: BrowserContext, page: Page, testInfo: TestInfo) { constructor(context: BrowserContext, page: Page, testInfo: TestInfo) {
this.context = context this.context = context
@ -46,7 +64,7 @@ export class AuthenticatedApp {
} }
getInputFile = (fileName: string) => { getInputFile = (fileName: string) => {
return fsp.readFile( return fsp.readFile(
join('rust', 'kcl-lib', 'e2e', 'executor', 'inputs', fileName), path.join('rust', 'kcl-lib', 'e2e', 'executor', 'inputs', fileName),
'utf-8' 'utf-8'
) )
} }
@ -59,101 +77,300 @@ export interface Fixtures {
scene: SceneFixture scene: SceneFixture
homePage: HomePageFixture homePage: HomePageFixture
} }
export class AuthenticatedTronApp {
public originalPage: Page
public page: Page
public browserContext: BrowserContext
public context: BrowserContext
public readonly testInfo: TestInfo
public electronApp: ElectronApplication | undefined
public readonly viewPortSize = { width: 1200, height: 500 }
public dir: string = ''
constructor( export class ElectronZoo {
browserContext: BrowserContext, public available: boolean = true
originalPage: Page, public electron!: ElectronApplication
testInfo: TestInfo public firstUrl = ''
) { public viewPortSize = { width: 1200, height: 500 }
this.page = originalPage public projectDirName = ''
this.originalPage = originalPage
this.browserContext = browserContext public page!: Page
// Will be overwritten in the initializer public context!: BrowserContext
this.context = browserContext
this.testInfo = testInfo constructor() {}
async makeAvailableAgain() {
// Help remote end by signaling we're done with the connection.
await this.page.evaluate(async () => {
return new Promise((resolve) => {
if (!window.engineCommandManager.engineConnection?.state?.type) {
return resolve(undefined)
} }
async initialise(
arg: {
fixtures: Partial<Fixtures>
folderSetupFn?: (projectDirName: string) => Promise<void>
cleanProjectDir?: boolean
appSettings?: DeepPartial<Settings>
} = { fixtures: {} }
) {
const { electronApp, page, context, dir } = await setupElectron({
testInfo: this.testInfo,
folderSetupFn: arg.folderSetupFn,
cleanProjectDir: arg.cleanProjectDir,
appSettings: arg.appSettings,
viewport: this.viewPortSize,
})
this.page = page
// These assignments "fix" some brokenness in the Playwright Workbench when window.engineCommandManager.tearDown()
// running against electron applications. // Keep polling (per js event tick) until state is Disconnected.
// The timeline is still broken but failure screenshots work again. const checkDisconnected = () => {
this.context = context // It's possible we never even created an engineConnection
// TODO: try to get this to work again for screenshots, but it messed with test ends when enabled // e.g. never left Projects view.
// Object.assign(this.browserContext, this.context)
this.electronApp = electronApp
this.dir = dir
// Easier to access throughout utils
this.page.dir = dir
// Setup localStorage, addCookies, reload
await setup(this.context, this.page, this.testInfo)
for (const key of unsafeTypedKeys(arg.fixtures)) {
const fixture = arg.fixtures[key]
if ( if (
!fixture || window.engineCommandManager?.engineConnection?.state.type ===
fixture instanceof AuthenticatedApp || 'disconnected'
fixture instanceof AuthenticatedTronApp ) {
) return resolve(undefined)
continue
fixture.reConstruct(page)
} }
setTimeout(checkDisconnected, 0)
}
checkDisconnected()
})
})
await this.context.tracing.stopChunk({ path: 'trace.zip' })
// Only after cleanup we're ready.
this.available = true
} }
close = async () => { async createInstanceIfMissing(testInfo: TestInfo) {
await this.electronApp?.close?.() // Create or otherwise clear the folder.
this.projectDirName = testInfo.outputPath('electron-test-projects-dir')
// We need to expose this in order for some tests that require folder
// creation and some code below.
const that = this
const options = {
args: ['.', '--no-sandbox'],
env: {
...process.env,
TEST_SETTINGS_FILE_KEY: this.projectDirName,
IS_PLAYWRIGHT: 'true',
},
...(process.env.ELECTRON_OVERRIDE_DIST_PATH
? {
executablePath:
process.env.ELECTRON_OVERRIDE_DIST_PATH + 'electron',
} }
debugPause = () => : {}),
new Promise(() => { ...(process.env.PLAYWRIGHT_RECORD_VIDEO
console.log('UN-RESOLVING PROMISE') ? {
recordVideo: {
dir: testInfo.snapshotPath(),
size: this.viewPortSize,
},
}
: {}),
}
// Do this once and then reuse window on subsequent calls.
if (!this.electron) {
this.electron = await electron.launch(options)
this.context = this.electron.context()
this.page = await this.electron.firstWindow()
await this.context.tracing.start({ screenshots: true, snapshots: true })
}
await this.context.tracing.startChunk()
await setup(this.context, this.page, testInfo)
await this.cleanProjectDir()
// Create a consistent way to resize the page across electron and web.
// (lee) I had to do everything in the book to make electron change its
// damn window size. I succeeded in making it consistently and reliably
// do it after a whole afternoon.
this.page.setBodyDimensions = async function (dims: {
width: number
height: number
}) {
await this.setViewportSize(dims)
await that.electron?.evaluateHandle(async ({ app }, dims) => {
// @ts-ignore sorry jon but see comment in main.ts why this is ignored
await app.resizeWindow(dims.width, dims.height)
}, dims)
return this.evaluate(async (dims: { width: number; height: number }) => {
await window.electron.resizeWindow(dims.width, dims.height)
window.document.body.style.width = dims.width + 'px'
window.document.body.style.height = dims.height + 'px'
window.document.documentElement.style.width = dims.width + 'px'
window.document.documentElement.style.height = dims.height + 'px'
}, dims)
}
await this.page.setBodyDimensions(this.viewPortSize)
this.context.folderSetupFn = async function (fn) {
return fn(that.projectDirName)
.then(() => that.page.reload())
.then(() => ({
dir: that.projectDirName,
}))
}
// We need to patch this because addInitScript will bind too late in our
// electron tests, never running. We need to call reload() after each call
// to guarantee it runs.
const oldContextAddInitScript = this.context.addInitScript
this.context.addInitScript = async function (a, b) {
// @ts-ignore pretty sure way out of tsc's type checking capabilities.
// This code works perfectly fine.
await oldContextAddInitScript.apply(this, [a, b])
await that.page.reload()
}
// No idea why we mix and match page and context's addInitScript but we do
const oldPageAddInitScript = this.page.addInitScript
this.page.addInitScript = async function (a: any, b: any) {
// @ts-ignore pretty sure way out of tsc's type checking capabilities.
// This code works perfectly fine.
await oldPageAddInitScript.apply(this, [a, b])
await that.page.reload()
}
if (!this.firstUrl) {
await this.page.getByText('Your Projects').count()
this.firstUrl = this.page.url()
}
// Due to the app controlling its own window context we need to inject new
// options and context here.
// NOTE TO LEE: Seems to destroy page context when calling an electron loadURL.
// await tronApp.electronApp.evaluate(({ app }) => {
// return app.reuseWindowForTest();
// });
await this.electron?.evaluate(({ app }, projectDirName) => {
// @ts-ignore can't declaration merge see main.ts
app.testProperty['TEST_SETTINGS_FILE_KEY'] = projectDirName
}, this.projectDirName)
// Always start at the root view
await this.page.goto(this.firstUrl)
// Force a hard reload, destroying the stream and other state
await this.page.reload()
}
async cleanProjectDir(appSettings?: DeepPartial<Settings>) {
try {
if (fs.existsSync(this.projectDirName)) {
await fsp.rm(this.projectDirName, { recursive: true })
}
} catch (e) {
console.error(e)
}
try {
await fsp.mkdir(this.projectDirName)
} catch (e) {
// Not a problem if it already exists.
}
const tempSettingsFilePath = path.join(
this.projectDirName,
SETTINGS_FILE_NAME
)
let settingsOverridesToml = ''
if (appSettings) {
settingsOverridesToml = TOML.stringify({
// @ts-expect-error
settings: {
...TEST_SETTINGS,
...appSettings,
app: {
...TEST_SETTINGS.app,
project_directory: this.projectDirName,
...appSettings.app,
},
},
}) })
} else {
settingsOverridesToml = TOML.stringify({
// @ts-expect-error
settings: {
...TEST_SETTINGS,
app: {
...TEST_SETTINGS.app,
project_directory: this.projectDirName,
},
},
})
}
await fsp.writeFile(tempSettingsFilePath, settingsOverridesToml)
}
} }
export const fixtures = { // If yee encounter this, please try to type it.
cmdBar: async ({ page }: { page: Page }, use: any) => { type FnUse = any
// eslint-disable-next-line react-hooks/rules-of-hooks
const fixturesForElectron = {
page: async (
{ tronApp }: { tronApp: ElectronZoo },
use: FnUse,
testInfo: TestInfo
) => {
await tronApp.createInstanceIfMissing(testInfo)
await use(tronApp.page)
await tronApp?.makeAvailableAgain()
},
context: async (
{ tronApp }: { tronApp: ElectronZoo },
use: FnUse,
testInfo: TestInfo
) => {
await tronApp.createInstanceIfMissing(testInfo)
await use(tronApp.context)
},
}
const fixturesForWeb = {
page: async (
{ page, context }: { page: Page; context: BrowserContext },
use: FnUse,
testInfo: TestInfo
) => {
page.setBodyDimensions = page.setViewportSize
// We do the same thing in ElectronZoo. addInitScript simply doesn't fire
// at the correct time, so we reload the page and it fires appropriately.
const oldPageAddInitScript = page.addInitScript
page.addInitScript = async function (...args) {
// @ts-expect-error
await oldPageAddInitScript.apply(this, args)
await page.reload()
}
const oldContextAddInitScript = context.addInitScript
context.addInitScript = async function (...args) {
// @ts-expect-error
await oldContextAddInitScript.apply(this, args)
await page.reload()
}
const webApp = new AuthenticatedApp(context, page, testInfo)
await webApp.initialise()
await use(page)
},
}
const fixturesBasedOnProcessEnvPlatform = {
cmdBar: async ({ page }: { page: Page }, use: FnUse) => {
await use(new CmdBarFixture(page)) await use(new CmdBarFixture(page))
}, },
editor: async ({ page }: { page: Page }, use: any) => { editor: async ({ page }: { page: Page }, use: FnUse) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
await use(new EditorFixture(page)) await use(new EditorFixture(page))
}, },
toolbar: async ({ page }: { page: Page }, use: any) => { toolbar: async ({ page }: { page: Page }, use: FnUse) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
await use(new ToolbarFixture(page)) await use(new ToolbarFixture(page))
}, },
scene: async ({ page }: { page: Page }, use: any) => { scene: async ({ page }: { page: Page }, use: FnUse) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
await use(new SceneFixture(page)) await use(new SceneFixture(page))
}, },
homePage: async ({ page }: { page: Page }, use: any) => { homePage: async ({ page }: { page: Page }, use: FnUse) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
await use(new HomePageFixture(page)) await use(new HomePageFixture(page))
}, },
} }
if (process.env.PLATFORM === 'web') {
Object.assign(fixturesBasedOnProcessEnvPlatform, fixturesForWeb)
} else {
Object.assign(fixturesBasedOnProcessEnvPlatform, fixturesForElectron)
}
export { fixturesBasedOnProcessEnvPlatform }

View File

@ -27,10 +27,6 @@ export class HomePageFixture {
constructor(page: Page) { constructor(page: Page) {
this.page = page this.page = page
this.reConstruct(page)
}
reConstruct = (page: Page) => {
this.page = page
this.projectSection = this.page.getByTestId('home-section') this.projectSection = this.page.getByTestId('home-section')
@ -96,8 +92,12 @@ export class HomePageFixture {
} }
} }
createAndGoToProject = async (projectTitle = 'project-$nnn') => { projectsLoaded = async () => {
await expect(this.projectSection).not.toHaveText('Loading your Projects...') await expect(this.projectSection).not.toHaveText('Loading your Projects...')
}
createAndGoToProject = async (projectTitle = 'project-$nnn') => {
await this.projectsLoaded()
await this.projectButtonNew.click() await this.projectButtonNew.click()
await this.projectTextName.click() await this.projectTextName.click()
await this.projectTextName.fill(projectTitle) await this.projectTextName.fill(projectTitle)

View File

@ -53,7 +53,12 @@ export class SceneFixture {
constructor(page: Page) { constructor(page: Page) {
this.page = page this.page = page
this.reConstruct(page) this.streamWrapper = page.getByTestId('stream')
this.networkToggleConnected = page.getByTestId('network-toggle-ok')
this.loadingIndicator = this.streamWrapper.getByTestId('loading')
this.startEditSketchBtn = page
.getByRole('button', { name: 'Start Sketch' })
.or(page.getByRole('button', { name: 'Edit Sketch' }))
} }
private _serialiseScene = async (): Promise<SceneSerialised> => { private _serialiseScene = async (): Promise<SceneSerialised> => {
const camera = await this.getCameraInfo() const camera = await this.getCameraInfo()
@ -72,17 +77,6 @@ export class SceneFixture {
.toEqual(expected) .toEqual(expected)
} }
reConstruct = (page: Page) => {
this.page = page
this.streamWrapper = page.getByTestId('stream')
this.networkToggleConnected = page.getByTestId('network-toggle-ok')
this.loadingIndicator = this.streamWrapper.getByTestId('loading')
this.startEditSketchBtn = page
.getByRole('button', { name: 'Start Sketch' })
.or(page.getByRole('button', { name: 'Edit Sketch' }))
}
makeMouseHelpers = ( makeMouseHelpers = (
x: number, x: number,
y: number, y: number,
@ -253,7 +247,7 @@ export class SceneFixture {
await u.openDebugPanel() await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]') await u.expectCmdLog('[data-message-type="execution-done"]')
await u.clearAndCloseDebugPanel() await u.closeDebugPanel()
await this.waitForExecutionDone() await this.waitForExecutionDone()
await expect(this.startEditSketchBtn).not.toBeDisabled() await expect(this.startEditSketchBtn).not.toBeDisabled()

View File

@ -37,13 +37,12 @@ export class ToolbarFixture {
featureTreeId = 'feature-tree' as const featureTreeId = 'feature-tree' as const
/** The pane element for the Feature Tree */ /** The pane element for the Feature Tree */
featureTreePane!: Locator featureTreePane!: Locator
gizmo!: Locator
gizmoDisabled!: Locator
constructor(page: Page) { constructor(page: Page) {
this.page = page this.page = page
this.reConstruct(page)
}
reConstruct = (page: Page) => {
this.page = page
this.extrudeButton = page.getByTestId('extrude') this.extrudeButton = page.getByTestId('extrude')
this.loftButton = page.getByTestId('loft') this.loftButton = page.getByTestId('loft')
this.sweepButton = page.getByTestId('sweep') this.sweepButton = page.getByTestId('sweep')
@ -67,6 +66,13 @@ export class ToolbarFixture {
this.filePane = page.locator('#files-pane') this.filePane = page.locator('#files-pane')
this.featureTreePane = page.locator('#feature-tree-pane') this.featureTreePane = page.locator('#feature-tree-pane')
this.fileCreateToast = page.getByText('Successfully created') this.fileCreateToast = page.getByText('Successfully created')
// Note to test writers: having two locators like this is preferable to one
// which changes another el property because it means our test "signal" is
// completely decoupled from the elements themselves. It means the same
// element or two different elements can represent these states.
this.gizmo = page.getByTestId('gizmo')
this.gizmoDisabled = page.getByTestId('gizmo-disabled')
} }
get editSketchBtn() { get editSketchBtn() {
@ -86,6 +92,18 @@ export class ToolbarFixture {
startSketchPlaneSelection = async () => startSketchPlaneSelection = async () =>
doAndWaitForImageDiff(this.page, () => this.startSketchBtn.click(), 500) doAndWaitForImageDiff(this.page, () => this.startSketchBtn.click(), 500)
waitUntilSketchingReady = async () => {
await expect(this.gizmoDisabled).toBeVisible()
}
startSketchThenCallbackThenWaitUntilReady = async (
cb: () => Promise<void>
) => {
await this.startSketchBtn.click()
await cb()
await this.waitUntilSketchingReady()
}
exitSketch = async () => { exitSketch = async () => {
await this.exitSketchBtn.click() await this.exitSketchBtn.click()
await expect( await expect(

View File

@ -21,58 +21,54 @@ import { expectPixelColor } from './fixtures/sceneFixture'
// we must set it to empty for the tests where we want to see the onboarding immediately. // we must set it to empty for the tests where we want to see the onboarding immediately.
test.describe('Onboarding tests', () => { test.describe('Onboarding tests', () => {
test( test('Onboarding code is shown in the editor', async ({
'Onboarding code is shown in the editor', page,
{ homePage,
appSettings: { tronApp,
}) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: { app: {
onboarding_status: '', onboarding_status: '',
}, },
}, })
cleanProjectDir: true,
},
async ({ page, homePage }) => {
const u = await getUtils(page) const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene() await homePage.goToModelingScene()
// Test that the onboarding pane loaded // Test that the onboarding pane loaded
await expect( await expect(page.getByText('Welcome to Modeling App! This')).toBeVisible()
page.getByText('Welcome to Modeling App! This')
).toBeVisible()
// Test that the onboarding pane loaded // Test that the onboarding pane loaded
await expect( await expect(page.getByText('Welcome to Modeling App! This')).toBeVisible()
page.getByText('Welcome to Modeling App! This')
).toBeVisible()
// *and* that the code is shown in the editor // *and* that the code is shown in the editor
await expect(page.locator('.cm-content')).toContainText( await expect(page.locator('.cm-content')).toContainText('// Shelf Bracket')
'// Shelf Bracket'
)
// Make sure the model loaded // Make sure the model loaded
const XYPlanePoint = { x: 774, y: 116 } as const const XYPlanePoint = { x: 774, y: 116 } as const
const modelColor: [number, number, number] = [45, 45, 45] const modelColor: [number, number, number] = [45, 45, 45]
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y) await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
expect(await u.getGreatestPixDiff(XYPlanePoint, modelColor)).toBeLessThan( expect(await u.getGreatestPixDiff(XYPlanePoint, modelColor)).toBeLessThan(8)
8 })
)
}
)
test( test(
'Desktop: fresh onboarding executes and loads', 'Desktop: fresh onboarding executes and loads',
{ {
tag: '@electron', tag: '@electron',
appSettings: { },
async ({ page, tronApp }) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: { app: {
onboarding_status: '', onboarding_status: '',
}, },
}, })
cleanProjectDir: true,
},
async ({ page }) => {
const u = await getUtils(page) const u = await getUtils(page)
const viewportSize = { width: 1200, height: 500 } const viewportSize = { width: 1200, height: 500 }
@ -107,22 +103,30 @@ test.describe('Onboarding tests', () => {
} }
) )
test( test('Code resets after confirmation', async ({
'Code resets after confirmation', context,
{ page,
cleanProjectDir: true, homePage,
}, tronApp,
async ({ context, page, homePage }) => { scene,
cmdBar,
}) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir()
const initialCode = `sketch001 = startSketchOn('XZ')` const initialCode = `sketch001 = startSketchOn('XZ')`
// Load the page up with some code so we see the confirmation warning // Load the page up with some code so we see the confirmation warning
// when we go to replay onboarding // when we go to replay onboarding
await context.addInitScript((code) => { await page.addInitScript((code) => {
localStorage.setItem('persistCode', code) localStorage.setItem('persistCode', code)
}, initialCode) }, initialCode)
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene() await homePage.goToModelingScene()
await scene.connectionEstablished()
// Replay the onboarding // Replay the onboarding
await page.getByRole('link', { name: 'Settings' }).last().click() await page.getByRole('link', { name: 'Settings' }).last().click()
@ -142,26 +146,27 @@ test.describe('Onboarding tests', () => {
// Ensure we see the introduction and that the code has been reset // Ensure we see the introduction and that the code has been reset
await expect(page.getByText('Welcome to Modeling App!')).toBeVisible() await expect(page.getByText('Welcome to Modeling App!')).toBeVisible()
await expect(page.locator('.cm-content')).toContainText( await expect(page.locator('.cm-content')).toContainText('// Shelf Bracket')
'// Shelf Bracket'
)
// There used to be old code here that checked if we stored the reset // There used to be old code here that checked if we stored the reset
// code into localStorage but that isn't the case on desktop. It gets // code into localStorage but that isn't the case on desktop. It gets
// saved to the file system, which we have other tests for. // saved to the file system, which we have other tests for.
} })
)
test( test('Click through each onboarding step and back', async ({
'Click through each onboarding step and back', context,
{ page,
appSettings: { homePage,
tronApp,
}) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: { app: {
onboarding_status: '', onboarding_status: '',
}, },
}, })
},
async ({ context, page, homePage }) => {
// Override beforeEach test setup // Override beforeEach test setup
await context.addInitScript( await context.addInitScript(
async ({ settingsKey, settings }) => { async ({ settingsKey, settings }) => {
@ -181,9 +186,7 @@ test.describe('Onboarding tests', () => {
await homePage.goToModelingScene() await homePage.goToModelingScene()
// Test that the onboarding pane loaded // Test that the onboarding pane loaded
await expect( await expect(page.getByText('Welcome to Modeling App! This')).toBeVisible()
page.getByText('Welcome to Modeling App! This')
).toBeVisible()
const nextButton = page.getByTestId('onboarding-next') const nextButton = page.getByTestId('onboarding-next')
const prevButton = page.getByTestId('onboarding-prev') const prevButton = page.getByTestId('onboarding-prev')
@ -205,20 +208,23 @@ test.describe('Onboarding tests', () => {
// Test that the onboarding pane is gone // Test that the onboarding pane is gone
await expect(page.getByTestId('onboarding-content')).not.toBeVisible() await expect(page.getByTestId('onboarding-content')).not.toBeVisible()
await expect.poll(() => page.url()).not.toContain('/onboarding') await expect.poll(() => page.url()).not.toContain('/onboarding')
} })
)
test( test('Onboarding redirects and code updating', async ({
'Onboarding redirects and code updating', context,
{ page,
appSettings: { homePage,
tronApp,
}) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: { app: {
onboarding_status: '/export', onboarding_status: '/export',
}, },
}, })
cleanProjectDir: true,
},
async ({ context, page, homePage }) => {
const originalCode = 'sigmaAllow = 15000' const originalCode = 'sigmaAllow = 15000'
// Override beforeEach test setup // Override beforeEach test setup
@ -260,21 +266,22 @@ test.describe('Onboarding tests', () => {
await page.locator('[data-testid="onboarding-next"]').hover() await page.locator('[data-testid="onboarding-next"]').hover()
await page.locator('[data-testid="onboarding-next"]').click() await page.locator('[data-testid="onboarding-next"]').click()
await expect(page.locator('.cm-content')).toHaveText(/.+/) await expect(page.locator('.cm-content')).toHaveText(/.+/)
} })
)
test( test('Onboarding code gets reset to demo on Interactive Numbers step', async ({
'Onboarding code gets reset to demo on Interactive Numbers step', page,
{ homePage,
appSettings: { tronApp,
}) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: { app: {
onboarding_status: '/parametric-modeling', onboarding_status: '/parametric-modeling',
}, },
}, })
cleanProjectDir: true,
},
async ({ page, homePage }) => {
const u = await getUtils(page) const u = await getUtils(page)
const badCode = `// This is bad code we shouldn't see` const badCode = `// This is bad code we shouldn't see`
@ -307,23 +314,24 @@ test.describe('Onboarding tests', () => {
// Check that the code has been reset // Check that the code has been reset
await expect(u.codeLocator).toHaveText(bracketNoNewLines) await expect(u.codeLocator).toHaveText(bracketNoNewLines)
} })
)
// (lee) The two avatar tests are weird because even on main, we don't have // (lee) The two avatar tests are weird because even on main, we don't have
// anything to do with the avatar inside the onboarding test. Due to the // anything to do with the avatar inside the onboarding test. Due to the
// low impact of an avatar not showing I'm changing this to fixme. // low impact of an avatar not showing I'm changing this to fixme.
test.fixme( test.fixme(
'Avatar text updates depending on image load success', 'Avatar text updates depending on image load success',
{ async ({ context, page, homePage, tronApp }) => {
appSettings: { if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: { app: {
onboarding_status: '', onboarding_status: '',
}, },
}, })
cleanProjectDir: true,
},
async ({ context, page, homePage }) => {
// Override beforeEach test setup // Override beforeEach test setup
await context.addInitScript( await context.addInitScript(
async ({ settingsKey, settings }) => { async ({ settingsKey, settings }) => {
@ -388,15 +396,16 @@ test.describe('Onboarding tests', () => {
test.fixme( test.fixme(
"Avatar text doesn't mention avatar when no avatar", "Avatar text doesn't mention avatar when no avatar",
{ async ({ context, page, homePage, tronApp }) => {
appSettings: { if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: { app: {
onboarding_status: '', onboarding_status: '',
}, },
}, })
cleanProjectDir: true,
},
async ({ context, page, homePage }) => {
// Override beforeEach test setup // Override beforeEach test setup
await context.addInitScript( await context.addInitScript(
async ({ settingsKey, settings }) => { async ({ settingsKey, settings }) => {
@ -444,15 +453,17 @@ test.describe('Onboarding tests', () => {
test.fixme( test.fixme(
'Restarting onboarding on desktop takes one attempt', 'Restarting onboarding on desktop takes one attempt',
{ async ({ context, page, tronApp }) => {
appSettings: { if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: { app: {
onboarding_status: 'dismissed', onboarding_status: 'dismissed',
}, },
}, })
cleanProjectDir: true,
},
async ({ context, page }) => {
await context.folderSetupFn(async (dir) => { await context.folderSetupFn(async (dir) => {
const routerTemplateDir = join(dir, 'router-template-slate') const routerTemplateDir = join(dir, 'router-template-slate')
await fsp.mkdir(routerTemplateDir, { recursive: true }) await fsp.mkdir(routerTemplateDir, { recursive: true })

View File

@ -1,4 +1,5 @@
import { test, expect, Page } from './zoo-test' import { Page } from '@playwright/test'
import { test, expect } from './zoo-test'
import { EditorFixture } from './fixtures/editorFixture' import { EditorFixture } from './fixtures/editorFixture'
import { SceneFixture } from './fixtures/sceneFixture' import { SceneFixture } from './fixtures/sceneFixture'
import { ToolbarFixture } from './fixtures/toolbarFixture' import { ToolbarFixture } from './fixtures/toolbarFixture'

View File

@ -163,7 +163,7 @@ test(
.poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), { .poll(() => u.getGreatestPixDiff(pointOnModel, [85, 85, 85]), {
timeout: 10_000, timeout: 10_000,
}) })
.toBeLessThan(15) .toBeLessThan(20)
}) })
await test.step('Clicking the logo takes us back to the projects page / home', async () => { await test.step('Clicking the logo takes us back to the projects page / home', async () => {
@ -464,7 +464,11 @@ test.describe('Can export from electron app', () => {
test( test(
`Can export using ${method}`, `Can export using ${method}`,
{ tag: ['@electron', '@skipLocalEngine'] }, { tag: ['@electron', '@skipLocalEngine'] },
async ({ context, page }, testInfo) => { async ({ context, page, tronApp }, testInfo) => {
if (!tronApp) {
fail()
}
await context.folderSetupFn(async (dir) => { await context.folderSetupFn(async (dir) => {
const bracketDir = path.join(dir, 'bracket') const bracketDir = path.join(dir, 'bracket')
await fsp.mkdir(bracketDir, { recursive: true }) await fsp.mkdir(bracketDir, { recursive: true })
@ -516,6 +520,7 @@ test.describe('Can export from electron app', () => {
storage: 'embedded', storage: 'embedded',
presentation: 'pretty', presentation: 'pretty',
}, },
tronApp.projectDirName,
page, page,
method method
) )
@ -523,7 +528,7 @@ test.describe('Can export from electron app', () => {
}) })
const filepath = path.resolve( const filepath = path.resolve(
getPlaywrightDownloadDir(page), getPlaywrightDownloadDir(tronApp.projectDirName),
'main.gltf' 'main.gltf'
) )
@ -781,6 +786,7 @@ test(
page.on('console', console.log) page.on('console', console.log)
await expect(page.getByText('router-template-slate')).toBeVisible() await expect(page.getByText('router-template-slate')).toBeVisible()
await expect(page.getByText('Loading your Projects...')).not.toBeVisible()
await expect(page.getByText('Your Projects')).toBeVisible() await expect(page.getByText('Your Projects')).toBeVisible()
await page.keyboard.press('Delete') await page.keyboard.press('Delete')
@ -858,7 +864,7 @@ test.describe(`Project management commands`, () => {
test( test(
`Delete from project page`, `Delete from project page`,
{ tag: '@electron' }, { tag: '@electron' },
async ({ context, page }, testInfo) => { async ({ context, page, scene, cmdBar }, testInfo) => {
const projectName = `my_project_to_delete` const projectName = `my_project_to_delete`
await context.folderSetupFn(async (dir) => { await context.folderSetupFn(async (dir) => {
await fsp.mkdir(`${dir}/${projectName}`, { recursive: true }) await fsp.mkdir(`${dir}/${projectName}`, { recursive: true })
@ -887,6 +893,8 @@ test.describe(`Project management commands`, () => {
await projectHomeLink.click() await projectHomeLink.click()
await u.waitForPageLoad() await u.waitForPageLoad()
await scene.connectionEstablished()
await scene.settled(cmdBar)
}) })
await test.step(`Run delete command via command palette`, async () => { await test.step(`Run delete command via command palette`, async () => {
@ -909,7 +917,7 @@ test.describe(`Project management commands`, () => {
test( test(
`Rename from home page`, `Rename from home page`,
{ tag: '@electron' }, { tag: '@electron' },
async ({ context, page }, testInfo) => { async ({ context, page, homePage }, testInfo) => {
const projectName = `my_project_to_rename` const projectName = `my_project_to_rename`
await context.folderSetupFn(async (dir) => { await context.folderSetupFn(async (dir) => {
await fsp.mkdir(`${dir}/${projectName}`, { recursive: true }) await fsp.mkdir(`${dir}/${projectName}`, { recursive: true })
@ -936,6 +944,7 @@ test.describe(`Project management commands`, () => {
await test.step(`Setup`, async () => { await test.step(`Setup`, async () => {
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
page.on('console', console.log) page.on('console', console.log)
await homePage.projectsLoaded()
await expect(projectHomeLink).toBeVisible() await expect(projectHomeLink).toBeVisible()
}) })
@ -1682,7 +1691,11 @@ test(
test( test(
'You can change the root projects directory and nothing is lost', 'You can change the root projects directory and nothing is lost',
{ tag: '@electron' }, { tag: '@electron' },
async ({ context, page, electronApp }, testInfo) => { async ({ context, page, tronApp, homePage }, testInfo) => {
if (!tronApp) {
fail()
}
await context.folderSetupFn(async (dir) => { await context.folderSetupFn(async (dir) => {
await Promise.all([ await Promise.all([
fsp.mkdir(`${dir}/router-template-slate`, { recursive: true }), fsp.mkdir(`${dir}/router-template-slate`, { recursive: true }),
@ -1712,6 +1725,8 @@ test(
await fsp.rm(newProjectDirName, { recursive: true }) await fsp.rm(newProjectDirName, { recursive: true })
} }
await homePage.projectsLoaded()
await test.step('We can change the root project directory', async () => { await test.step('We can change the root project directory', async () => {
// expect to see the project directory settings link // expect to see the project directory settings link
await expect( await expect(
@ -1725,7 +1740,7 @@ test(
.locator('section#projectDirectory input') .locator('section#projectDirectory input')
.inputValue() .inputValue()
const handleFile = electronApp?.evaluate( const handleFile = tronApp.electron.evaluate(
async ({ dialog }, filePaths) => { async ({ dialog }, filePaths) => {
dialog.showOpenDialog = () => dialog.showOpenDialog = () =>
Promise.resolve({ canceled: false, filePaths }) Promise.resolve({ canceled: false, filePaths })
@ -1741,6 +1756,8 @@ test(
await page.getByTestId('settings-close-button').click() await page.getByTestId('settings-close-button').click()
await homePage.projectsLoaded()
await expect(page.getByText('No Projects found')).toBeVisible() await expect(page.getByText('No Projects found')).toBeVisible()
await createProject({ name: 'project-000', page, returnHome: true }) await createProject({ name: 'project-000', page, returnHome: true })
await expect( await expect(
@ -1755,7 +1772,7 @@ test(
await page.getByTestId('project-directory-settings-link').click() await page.getByTestId('project-directory-settings-link').click()
const handleFile = electronApp?.evaluate( const handleFile = tronApp.electron.evaluate(
async ({ dialog }, filePaths) => { async ({ dialog }, filePaths) => {
dialog.showOpenDialog = () => dialog.showOpenDialog = () =>
Promise.resolve({ canceled: false, filePaths }) Promise.resolve({ canceled: false, filePaths })
@ -1767,6 +1784,7 @@ test(
await page.getByTestId('project-directory-button').click() await page.getByTestId('project-directory-button').click()
await handleFile await handleFile
await homePage.projectsLoaded()
await expect(page.locator('section#projectDirectory input')).toHaveValue( await expect(page.locator('section#projectDirectory input')).toHaveValue(
originalProjectDirName originalProjectDirName
) )
@ -2000,8 +2018,8 @@ test(
test( test(
'Settings persist across restarts', 'Settings persist across restarts',
{ tag: '@electron', cleanProjectDir: true }, { tag: '@electron' },
async ({ page }, testInfo) => { async ({ page, scene, cmdBar }, testInfo) => {
await test.step('We can change a user setting like theme', async () => { await test.step('We can change a user setting like theme', async () => {
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
@ -2014,6 +2032,10 @@ test(
await expect(page.getByTestId('app-theme')).toHaveValue('dark') await expect(page.getByTestId('app-theme')).toHaveValue('dark')
await page.getByTestId('app-theme').selectOption('light') await page.getByTestId('app-theme').selectOption('light')
await expect(page.getByTestId('app-theme')).toHaveValue('light')
// Give time to system for writing to a persistent store
await page.waitForTimeout(1000)
}) })
await test.step('Starting the app again and we can see the same theme', async () => { await test.step('Starting the app again and we can see the same theme', async () => {

View File

@ -1,4 +1,5 @@
import { test, expect, Page } from './zoo-test' import { Page } from '@playwright/test'
import { test, expect } from './zoo-test'
import path from 'path' import path from 'path'
import * as fsp from 'fs/promises' import * as fsp from 'fs/promises'
import { getUtils, executorInputPath } from './test-utils' import { getUtils, executorInputPath } from './test-utils'

View File

@ -1,4 +1,5 @@
import { test, expect, Page } from './zoo-test' import { Page } from '@playwright/test'
import { test, expect } from './zoo-test'
import fs from 'node:fs/promises' import fs from 'node:fs/promises'
import path from 'node:path' import path from 'node:path'
import { HomePageFixture } from './fixtures/homePageFixture' import { HomePageFixture } from './fixtures/homePageFixture'
@ -2153,6 +2154,8 @@ extrude001 = extrude(profile003, length = 5)
await page.setBodyDimensions({ width: 1000, height: 500 }) await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene() await homePage.goToModelingScene()
await page.waitForTimeout(5000)
await expect( await expect(
page.getByRole('button', { name: 'Start Sketch' }) page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled() ).not.toBeDisabled()
@ -2165,7 +2168,7 @@ extrude001 = extrude(profile003, length = 5)
await page.waitForTimeout(600) await page.waitForTimeout(600)
await editor.expectEditor.toContain(`sketch001 = startSketchOn('XZ')`) await editor.expectEditor.toContain(`sketch001 = startSketchOn('XZ')`)
await toolbar.exitSketchBtn.click() await toolbar.exitSketch()
await editor.expectEditor.not.toContain(`sketch001 = startSketchOn('XZ')`) await editor.expectEditor.not.toContain(`sketch001 = startSketchOn('XZ')`)
@ -2181,6 +2184,8 @@ extrude001 = extrude(profile003, length = 5)
)` )`
) )
await scene.settled(cmdBar)
await scene.expectPixelColor([255, 255, 255], { x: 633, y: 211 }, 15) await scene.expectPixelColor([255, 255, 255], { x: 633, y: 211 }, 15)
}) })
}) })

View File

@ -31,8 +31,7 @@ test.beforeEach(async ({ page, context }) => {
// Help engine-manager: tear shit down. // Help engine-manager: tear shit down.
test.afterEach(async ({ page }) => { test.afterEach(async ({ page }) => {
await page.evaluate(() => { await page.evaluate(() => {
// @ts-expect-error window.engineCommandManager.tearDown()
window.tearDown()
}) })
}) })
@ -45,7 +44,11 @@ test.setTimeout(60_000)
test.skip( test.skip(
'exports of each format should work', 'exports of each format should work',
{ tag: ['@snapshot', '@skipWin', '@skipMacos'] }, { tag: ['@snapshot', '@skipWin', '@skipMacos'] },
async ({ page, context, scene, cmdBar }) => { async ({ page, context, scene, cmdBar, tronApp }) => {
if (!tronApp) {
fail()
}
// FYI this test doesn't work with only engine running locally // FYI this test doesn't work with only engine running locally
// And you will need to have the KittyCAD CLI installed // And you will need to have the KittyCAD CLI installed
const u = await getUtils(page) const u = await getUtils(page)
@ -134,6 +137,7 @@ part001 = startSketchOn('-XZ')
storage: 'ascii', storage: 'ascii',
units: 'in', units: 'in',
}, },
tronApp.projectDirName,
page page
) )
) )
@ -146,6 +150,7 @@ part001 = startSketchOn('-XZ')
selection: { type: 'default_scene' }, selection: { type: 'default_scene' },
units: 'in', units: 'in',
}, },
tronApp.projectDirName,
page page
) )
) )
@ -158,6 +163,7 @@ part001 = startSketchOn('-XZ')
selection: { type: 'default_scene' }, selection: { type: 'default_scene' },
units: 'in', units: 'in',
}, },
tronApp.projectDirName,
page page
) )
) )
@ -170,6 +176,7 @@ part001 = startSketchOn('-XZ')
units: 'in', units: 'in',
selection: { type: 'default_scene' }, selection: { type: 'default_scene' },
}, },
tronApp.projectDirName,
page page
) )
) )
@ -182,6 +189,7 @@ part001 = startSketchOn('-XZ')
units: 'in', units: 'in',
selection: { type: 'default_scene' }, selection: { type: 'default_scene' },
}, },
tronApp.projectDirName,
page page
) )
) )
@ -193,6 +201,7 @@ part001 = startSketchOn('-XZ')
coords: sysType, coords: sysType,
units: 'in', units: 'in',
}, },
tronApp.projectDirName,
page page
) )
) )
@ -203,6 +212,7 @@ part001 = startSketchOn('-XZ')
storage: 'embedded', storage: 'embedded',
presentation: 'pretty', presentation: 'pretty',
}, },
tronApp.projectDirName,
page page
) )
) )
@ -213,6 +223,7 @@ part001 = startSketchOn('-XZ')
storage: 'binary', storage: 'binary',
presentation: 'pretty', presentation: 'pretty',
}, },
tronApp.projectDirName,
page page
) )
) )
@ -223,6 +234,7 @@ part001 = startSketchOn('-XZ')
storage: 'standard', storage: 'standard',
presentation: 'pretty', presentation: 'pretty',
}, },
tronApp.projectDirName,
page page
) )
) )

View File

@ -84,7 +84,7 @@ test.describe('Test network and connection issues', () => {
'Engine disconnect & reconnect in sketch mode', 'Engine disconnect & reconnect in sketch mode',
{ tag: '@skipLocalEngine' }, { tag: '@skipLocalEngine' },
async ({ page, homePage }) => { async ({ page, homePage }) => {
// TODO: Don't skip Mac for these. After `window.tearDown` is working in Safari, these should work on webkit // TODO: Don't skip Mac for these. After `window.engineCommandManager.tearDown` is working in Safari, these should work on webkit
const networkToggle = page.getByTestId('network-toggle') const networkToggle = page.getByTestId('network-toggle')
const u = await getUtils(page) const u = await getUtils(page)

View File

@ -5,8 +5,9 @@ import {
_electron as electron, _electron as electron,
ElectronApplication, ElectronApplication,
Locator, Locator,
Page,
} from '@playwright/test' } from '@playwright/test'
import { test, Page } from './zoo-test' import { test } from './zoo-test'
import { EngineCommand } from 'lang/std/artifactGraph' import { EngineCommand } from 'lang/std/artifactGraph'
import fsp from 'fs/promises' import fsp from 'fs/promises'
import fsSync from 'fs' import fsSync from 'fs'
@ -337,7 +338,7 @@ export const getMovementUtils = (opts: any) => {
async function waitForAuthAndLsp(page: Page) { async function waitForAuthAndLsp(page: Page) {
const waitForLspPromise = page.waitForEvent('console', { const waitForLspPromise = page.waitForEvent('console', {
predicate: async (message) => { predicate: async (message: any) => {
// it would be better to wait for a message that the kcl lsp has started by looking for the message message.text().includes('[lsp] [window/logMessage]') // it would be better to wait for a message that the kcl lsp has started by looking for the message message.text().includes('[lsp] [window/logMessage]')
// but that doesn't seem to make it to the console for macos/safari :( // but that doesn't seem to make it to the console for macos/safari :(
if (message.text().includes('start kcl lsp')) { if (message.text().includes('start kcl lsp')) {
@ -420,7 +421,7 @@ export async function getUtils(page: Page, test_?: typeof test) {
const overlay = page.locator(locator) const overlay = page.locator(locator)
const bbox = await overlay const bbox = await overlay
.boundingBox({ timeout: 5_000 }) .boundingBox({ timeout: 5_000 })
.then((box) => ({ ...box, x: box?.x || 0, y: box?.y || 0 })) .then((box: any) => ({ ...box, x: box?.x || 0, y: box?.y || 0 }))
const angle = Number(await overlay.getAttribute('data-overlay-angle')) const angle = Number(await overlay.getAttribute('data-overlay-angle'))
const angleXOffset = Math.cos(((angle - 180) * Math.PI) / 180) * px const angleXOffset = Math.cos(((angle - 180) * Math.PI) / 180) * px
const angleYOffset = Math.sin(((angle - 180) * Math.PI) / 180) * px const angleYOffset = Math.sin(((angle - 180) * Math.PI) / 180) * px
@ -437,7 +438,7 @@ export async function getUtils(page: Page, test_?: typeof test) {
page page
.locator(locator) .locator(locator)
.boundingBox({ timeout: 5_000 }) .boundingBox({ timeout: 5_000 })
.then((box) => ({ ...box, x: box?.x || 0, y: box?.y || 0 })), .then((box: any) => ({ ...box, x: box?.x || 0, y: box?.y || 0 })),
codeLocator: page.locator('.cm-content'), codeLocator: page.locator('.cm-content'),
crushKclCodeIntoOneLineAndThenMaybeSome: async () => { crushKclCodeIntoOneLineAndThenMaybeSome: async () => {
const code = await page.locator('.cm-content').innerText() const code = await page.locator('.cm-content').innerText()
@ -504,7 +505,7 @@ export async function getUtils(page: Page, test_?: typeof test) {
) => { ) => {
if (cdpSession === null) { if (cdpSession === null) {
// Use a fail safe if we can't simulate disconnect (on Safari) // Use a fail safe if we can't simulate disconnect (on Safari)
return page.evaluate('window.tearDown()') return page.evaluate('window.engineCommandManager.tearDown()')
} }
return cdpSession?.send( return cdpSession?.send(
@ -631,7 +632,7 @@ export async function getUtils(page: Page, test_?: typeof test) {
panesOpen: async (paneIds: PaneId[]) => { panesOpen: async (paneIds: PaneId[]) => {
return test?.step(`Setting ${paneIds} panes to be open`, async () => { return test?.step(`Setting ${paneIds} panes to be open`, async () => {
await page.addInitScript( await page.addInitScript(
({ PERSIST_MODELING_CONTEXT, paneIds }) => { ({ PERSIST_MODELING_CONTEXT, paneIds }: any) => {
localStorage.setItem( localStorage.setItem(
PERSIST_MODELING_CONTEXT, PERSIST_MODELING_CONTEXT,
JSON.stringify({ openPanes: paneIds }) JSON.stringify({ openPanes: paneIds })
@ -722,14 +723,14 @@ export const makeTemplate: (
const PLAYWRIGHT_DOWNLOAD_DIR = 'downloads-during-playwright' const PLAYWRIGHT_DOWNLOAD_DIR = 'downloads-during-playwright'
export const getPlaywrightDownloadDir = (page: Page) => { export const getPlaywrightDownloadDir = (rootDir: string) => {
return path.resolve(page.dir, PLAYWRIGHT_DOWNLOAD_DIR) return path.resolve(rootDir, PLAYWRIGHT_DOWNLOAD_DIR)
} }
const moveDownloadedFileTo = async (page: Page, toLocation: string) => { const moveDownloadedFileTo = async (rootDir: string, toLocation: string) => {
await fsp.mkdir(path.dirname(toLocation), { recursive: true }) await fsp.mkdir(path.dirname(toLocation), { recursive: true })
const downloadDir = getPlaywrightDownloadDir(page) const downloadDir = getPlaywrightDownloadDir(rootDir)
// Expect there to be at least one file // Expect there to be at least one file
await expect await expect
@ -756,6 +757,7 @@ export interface Paths {
export const doExport = async ( export const doExport = async (
output: Models['OutputFormat_type'], output: Models['OutputFormat_type'],
rootDir: string,
page: Page, page: Page,
exportFrom: 'dropdown' | 'sidebarButton' | 'commandBar' = 'dropdown' exportFrom: 'dropdown' | 'sidebarButton' | 'commandBar' = 'dropdown'
): Promise<Paths> => { ): Promise<Paths> => {
@ -836,7 +838,7 @@ export const doExport = async (
// (declared in src/lib/exportSave) // (declared in src/lib/exportSave)
// To remain consistent with our old web tests, we want to move some downloads // To remain consistent with our old web tests, we want to move some downloads
// (images) to another directory. // (images) to another directory.
await moveDownloadedFileTo(page, downloadLocation) await moveDownloadedFileTo(rootDir, downloadLocation)
} }
return { return {
@ -859,12 +861,6 @@ export async function tearDown(page: Page, testInfo: TestInfo) {
downloadThroughput: -1, downloadThroughput: -1,
uploadThroughput: -1, uploadThroughput: -1,
}) })
// It seems it's best to give the browser about 3s to close things
// It's not super reliable but we have no real other choice for now
await page.waitForTimeout(3000)
await testInfo.tronApp?.close()
} }
// settingsOverrides may need to be augmented to take more generic items, // settingsOverrides may need to be augmented to take more generic items,
@ -936,107 +932,11 @@ let electronApp: ElectronApplication | undefined = undefined
let context: BrowserContext | undefined = undefined let context: BrowserContext | undefined = undefined
let page: Page | undefined = undefined let page: Page | undefined = undefined
export async function setupElectron({
testInfo,
cleanProjectDir = true,
appSettings,
viewport,
}: {
testInfo: TestInfo
folderSetupFn?: (projectDirName: string) => Promise<void>
cleanProjectDir?: boolean
appSettings?: DeepPartial<Settings>
viewport: {
width: number
height: number
}
}): Promise<{
electronApp: ElectronApplication
context: BrowserContext
page: Page
dir: string
}> {
// create or otherwise clear the folder
const projectDirName = testInfo.outputPath('electron-test-projects-dir')
try {
if (fsSync.existsSync(projectDirName) && cleanProjectDir) {
await fsp.rm(projectDirName, { recursive: true })
}
} catch (e) {
console.error(e)
}
if (cleanProjectDir) {
await fsp.mkdir(projectDirName)
}
const options = {
args: ['.', '--no-sandbox'],
env: {
...process.env,
TEST_SETTINGS_FILE_KEY: projectDirName,
IS_PLAYWRIGHT: 'true',
},
...(process.env.ELECTRON_OVERRIDE_DIST_PATH
? { executablePath: process.env.ELECTRON_OVERRIDE_DIST_PATH + 'electron' }
: {}),
...(process.env.PLAYWRIGHT_RECORD_VIDEO
? {
recordVideo: {
dir: testInfo.snapshotPath(),
size: viewport,
},
}
: {}),
}
// Do this once and then reuse window on subsequent calls.
if (!electronApp) {
electronApp = await electron.launch(options)
}
if (!context || !page) {
context = electronApp.context()
page = await electronApp.firstWindow()
context.on('console', console.log)
page.on('console', console.log)
}
if (cleanProjectDir) {
const tempSettingsFilePath = path.join(projectDirName, SETTINGS_FILE_NAME)
const settingsOverrides = settingsToToml(
appSettings
? {
settings: {
...TEST_SETTINGS,
...appSettings,
app: {
...TEST_SETTINGS.app,
project_directory: projectDirName,
...appSettings.app,
},
},
}
: {
settings: {
...TEST_SETTINGS,
app: {
...TEST_SETTINGS.app,
project_directory: projectDirName,
},
},
}
)
await fsp.writeFile(tempSettingsFilePath, settingsOverrides)
}
return { electronApp, page, context, dir: projectDirName }
}
function failOnConsoleErrors(page: Page, testInfo?: TestInfo) { function failOnConsoleErrors(page: Page, testInfo?: TestInfo) {
// enabled for chrome for now // enabled for chrome for now
if (page.context().browser()?.browserType().name() === 'chromium') { if (page.context().browser()?.browserType().name() === 'chromium') {
page.on('pageerror', (exception) => { // No idea wtf exception is
page.on('pageerror', (exception: any) => {
if (isErrorWhitelisted(exception)) { if (isErrorWhitelisted(exception)) {
return return
} }

View File

@ -1,4 +1,5 @@
import { test, expect, Page } from './zoo-test' import { Page } from '@playwright/test'
import { test, expect } from './zoo-test'
import { deg, getUtils, wiggleMove } from './test-utils' import { deg, getUtils, wiggleMove } from './test-utils'
import { LineInputsType } from 'lang/std/sketchcombos' import { LineInputsType } from 'lang/std/sketchcombos'

View File

@ -20,14 +20,20 @@ import { DeepPartial } from 'lib/types'
import { Settings } from '@rust/kcl-lib/bindings/Settings' import { Settings } from '@rust/kcl-lib/bindings/Settings'
test.describe('Testing settings', () => { test.describe('Testing settings', () => {
test( test('Stored settings are validated and fall back to defaults', async ({
'Stored settings are validated and fall back to defaults', page,
homePage,
tronApp,
}) => {
if (!tronApp) {
fail()
}
// Override beforeEach test setup // Override beforeEach test setup
// with corrupted settings // with corrupted settings
{ await tronApp.cleanProjectDir(
appSettings: TEST_SETTINGS_CORRUPTED as DeepPartial<Settings>, TEST_SETTINGS_CORRUPTED as DeepPartial<Settings>
}, )
async ({ page, homePage }) => {
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
// Check the settings were reset // Check the settings were reset
@ -47,8 +53,7 @@ test.describe('Testing settings', () => {
expect(storedSettings.settings?.project?.default_project_name).toBe( expect(storedSettings.settings?.project?.default_project_name).toBe(
'project-$nnn' 'project-$nnn'
) )
} })
)
// The behavior is actually broken. Parent always takes precedence // The behavior is actually broken. Parent always takes precedence
test.fixme( test.fixme(
@ -357,8 +362,6 @@ test.describe('Testing settings', () => {
`Load desktop app with no settings file`, `Load desktop app with no settings file`,
{ {
tag: '@electron', tag: '@electron',
// This is what makes no settings file get created
cleanProjectDir: false,
}, },
async ({ page }, testInfo) => { async ({ page }, testInfo) => {
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
@ -379,13 +382,17 @@ test.describe('Testing settings', () => {
`Load desktop app with a settings file, but no project directory setting`, `Load desktop app with a settings file, but no project directory setting`,
{ {
tag: '@electron', tag: '@electron',
appSettings: { },
async ({ context, page, tronApp }, testInfo) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: { app: {
theme_color: '259', theme_color: '259',
}, },
}, })
},
async ({ context, page }, testInfo) => {
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
// Selectors and constants // Selectors and constants
@ -405,15 +412,20 @@ test.describe('Testing settings', () => {
'user settings reload on external change, on project and modeling view', 'user settings reload on external change, on project and modeling view',
{ {
tag: '@electron', tag: '@electron',
appSettings: { },
async ({ context, page, tronApp }, testInfo) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: { app: {
// Doesn't matter what you set it to. It will // Doesn't matter what you set it to. It will
// default to 264.5 // default to 264.5
theme_color: '0', theme_color: '0',
}, },
}, })
},
async ({ context, page }, testInfo) => {
const { dir: projectDirName } = await context.folderSetupFn( const { dir: projectDirName } = await context.folderSetupFn(
async () => {} async () => {}
) )
@ -783,13 +795,20 @@ test.describe('Testing settings', () => {
}) })
}) })
test( test(`Changing system theme preferences (via media query) should update UI and stream`, async ({
`Changing system theme preferences (via media query) should update UI and stream`, page,
{ homePage,
tronApp,
}) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
// Override the settings so that the theme is set to `system` // Override the settings so that the theme is set to `system`
appSettings: TEST_SETTINGS_DEFAULT_THEME, ...TEST_SETTINGS_DEFAULT_THEME,
}, })
async ({ page, homePage }) => {
const u = await getUtils(page) const u = await getUtils(page)
// Selectors and constants // Selectors and constants
@ -829,29 +848,31 @@ test.describe('Testing settings', () => {
.poll(() => streamBackgroundPixelIsColor(darkBackgroundColor)) .poll(() => streamBackgroundPixelIsColor(darkBackgroundColor))
.toBeLessThan(15) .toBeLessThan(15)
}) })
} })
)
test( test(`Turning off "Show debug panel" with debug panel open leaves no phantom panel`, async ({
`Turning off "Show debug panel" with debug panel open leaves no phantom panel`, context,
{ page,
homePage,
tronApp,
}) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
// Override beforeEach test setup // Override beforeEach test setup
// with debug panel open // with debug panel open
// but "show debug panel" set to false // but "show debug panel" set to false
appSettings: {
...TEST_SETTINGS, ...TEST_SETTINGS,
app: { ...TEST_SETTINGS.app, show_debug_panel: false }, app: { ...TEST_SETTINGS.app, show_debug_panel: false },
modeling: { ...TEST_SETTINGS.modeling }, modeling: { ...TEST_SETTINGS.modeling },
}, })
},
async ({ context, page, homePage }) => {
const u = await getUtils(page) const u = await getUtils(page)
await context.addInitScript(async () => { await context.addInitScript(async () => {
localStorage.setItem( localStorage.setItem('persistModelingContext', '{"openPanes":["debug"]}')
'persistModelingContext',
'{"openPanes":["debug"]}'
)
}) })
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene() await homePage.goToModelingScene()
@ -903,8 +924,7 @@ test.describe('Testing settings', () => {
await expect(debugPaneButton).not.toBeVisible() await expect(debugPaneButton).not.toBeVisible()
await expect(resizeHandle).not.toBeVisible() await expect(resizeHandle).not.toBeVisible()
}) })
} })
)
test(`Change inline units setting`, async ({ test(`Change inline units setting`, async ({
page, page,

View File

@ -1,4 +1,5 @@
import { test, expect, Page } from './zoo-test' import { Page } from '@playwright/test'
import { test, expect } from './zoo-test'
import { getUtils, createProject } from './test-utils' import { getUtils, createProject } from './test-utils'
import { join } from 'path' import { join } from 'path'
import fs from 'fs' import fs from 'fs'

View File

@ -35,7 +35,7 @@ test.fixme('Units menu', async ({ page, homePage }) => {
test( test(
'Successful export shows a success toast', 'Successful export shows a success toast',
{ tag: '@skipLocalEngine' }, { tag: '@skipLocalEngine' },
async ({ page, homePage }) => { async ({ page, homePage, tronApp }) => {
// FYI this test doesn't work with only engine running locally // FYI this test doesn't work with only engine running locally
// And you will need to have the KittyCAD CLI installed // And you will need to have the KittyCAD CLI installed
const u = await getUtils(page) const u = await getUtils(page)
@ -92,12 +92,17 @@ part001 = startSketchOn('-XZ')
await page.waitForTimeout(1000) await page.waitForTimeout(1000)
await u.clearAndCloseDebugPanel() await u.clearAndCloseDebugPanel()
if (!tronApp?.projectDirName) {
fail()
}
await doExport( await doExport(
{ {
type: 'gltf', type: 'gltf',
storage: 'embedded', storage: 'embedded',
presentation: 'pretty', presentation: 'pretty',
}, },
tronApp?.projectDirName,
page page
) )
} }
@ -465,7 +470,7 @@ test('Delete key does not navigate back', async ({ page, homePage }) => {
await expect.poll(() => page.url()).not.toContain('/settings') await expect.poll(() => page.url()).not.toContain('/settings')
}) })
test('Sketch on face', async ({ page, homePage, scene, cmdBar }) => { test('Sketch on face', async ({ page, homePage, scene, cmdBar, toolbar }) => {
test.setTimeout(90_000) test.setTimeout(90_000)
const u = await getUtils(page) const u = await getUtils(page)
await page.addInitScript(async () => { await page.addInitScript(async () => {
@ -491,17 +496,12 @@ extrude001 = extrude(sketch001, length = 5 + 7)`
await page.setBodyDimensions({ width: 1200, height: 500 }) await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene() await homePage.goToModelingScene()
await scene.waitForExecutionDone() await scene.connectionEstablished()
await scene.settled(cmdBar)
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
await page.getByRole('button', { name: 'Start Sketch' }).click()
await page.waitForTimeout(300)
let previousCodeContent = await page.locator('.cm-content').innerText() let previousCodeContent = await page.locator('.cm-content').innerText()
await toolbar.startSketchThenCallbackThenWaitUntilReady(async () => {
await u.openAndClearDebugPanel() await u.openAndClearDebugPanel()
await u.doAndWaitForCmd( await u.doAndWaitForCmd(
() => page.mouse.click(625, 165), () => page.mouse.click(625, 165),
@ -510,6 +510,8 @@ extrude001 = extrude(sketch001, length = 5 + 7)`
) )
await page.waitForTimeout(150) await page.waitForTimeout(150)
await u.closeDebugPanel() await u.closeDebugPanel()
})
await page.waitForTimeout(300)
const firstClickPosition = [612, 238] const firstClickPosition = [612, 238]
const secondClickPosition = [661, 242] const secondClickPosition = [661, 242]

View File

@ -1,21 +1,11 @@
import { /* eslint-disable react-hooks/rules-of-hooks */
test as playwrightTestFn,
TestInfo as TestInfoPlaywright, import { test as playwrightTestFn, ElectronApplication } from '@playwright/test'
BrowserContext as BrowserContextPlaywright,
Page as PagePlaywright,
TestDetails as TestDetailsPlaywright,
PlaywrightTestArgs,
PlaywrightTestOptions,
PlaywrightWorkerArgs,
PlaywrightWorkerOptions,
ElectronApplication,
} from '@playwright/test'
import { import {
fixtures, fixturesBasedOnProcessEnvPlatform,
Fixtures, Fixtures,
AuthenticatedTronApp, ElectronZoo,
AuthenticatedApp,
} from './fixtures/fixtureSetup' } from './fixtures/fixtureSetup'
import { Settings } from '@rust/kcl-lib/bindings/Settings' import { Settings } from '@rust/kcl-lib/bindings/Settings'
@ -23,9 +13,6 @@ import { DeepPartial } from 'lib/types'
export { expect } from '@playwright/test' export { expect } from '@playwright/test'
declare module '@playwright/test' { declare module '@playwright/test' {
interface TestInfo {
tronApp?: AuthenticatedTronApp
}
interface BrowserContext { interface BrowserContext {
folderSetupFn: ( folderSetupFn: (
cb: (dir: string) => Promise<void> cb: (dir: string) => Promise<void>
@ -41,288 +28,29 @@ declare module '@playwright/test' {
} }
} }
export type TestInfo = TestInfoPlaywright // Each worker spawns a new thread, which will spawn its own ElectronZoo.
export type BrowserContext = BrowserContextPlaywright // So in some sense there is an implicit pool.
export type Page = PagePlaywright // For example, the variable just beneath this text is reused many times
export type TestDetails = TestDetailsPlaywright & { // *for one worker*.
cleanProjectDir?: boolean const electronZooInstance = new ElectronZoo()
appSettings?: DeepPartial<Settings>
}
// Our custom decorated Zoo test object. Makes it easier to add fixtures, and // Our custom decorated Zoo test object. Makes it easier to add fixtures, and
// switch between web and electron if needed. // switch between web and electron if needed.
const pwTestFnWithFixtures = playwrightTestFn.extend<Fixtures>(fixtures) const playwrightTestFnWithFixtures_ = playwrightTestFn.extend<{
tronApp?: ElectronZoo
// In JavaScript you cannot replace a function's body only (despite functions }>({
// are themselves objects, which you'd expect a body property or something...) tronApp: async ({}, use, testInfo) => {
// So we must redefine the function and then re-attach properties.
type PWFunction = (
args: PlaywrightTestArgs &
Fixtures &
PlaywrightWorkerArgs &
PlaywrightTestOptions &
PlaywrightWorkerOptions & {
electronApp?: ElectronApplication
},
testInfo: TestInfo
) => void | Promise<void>
let firstUrl = ''
export const test = (
desc: string,
objOrFn: PWFunction | TestDetails,
fnMaybe?: PWFunction
) => {
const hasTestConf = typeof objOrFn === 'object'
const fn = hasTestConf ? fnMaybe : objOrFn
return pwTestFnWithFixtures(
desc,
hasTestConf ? objOrFn : {},
async (
{
page,
context,
cmdBar,
editor,
toolbar,
scene,
homePage,
request,
playwright,
browser,
acceptDownloads,
bypassCSP,
colorScheme,
clientCertificates,
deviceScaleFactor,
extraHTTPHeaders,
geolocation,
hasTouch,
httpCredentials,
ignoreHTTPSErrors,
isMobile,
javaScriptEnabled,
locale,
offline,
permissions,
proxy,
storageState,
timezoneId,
userAgent,
viewport,
baseURL,
contextOptions,
actionTimeout,
navigationTimeout,
serviceWorkers,
testIdAttribute,
browserName,
defaultBrowserType,
headless,
channel,
launchOptions,
connectOptions,
screenshot,
trace,
video,
},
testInfo
) => {
// To switch to web, use PLATFORM=web environment variable.
// Only use this for debugging, since the playwright tracer is busted
// for electron.
let tronApp
if (process.env.PLATFORM === 'web') { if (process.env.PLATFORM === 'web') {
tronApp = new AuthenticatedApp(context, page, testInfo) await use(undefined)
} else {
tronApp = new AuthenticatedTronApp(context, page, testInfo)
}
const fixtures: Fixtures = { cmdBar, editor, toolbar, scene, homePage }
if (tronApp instanceof AuthenticatedTronApp) {
const options = {
fixtures,
}
if (hasTestConf) {
Object.assign(options, {
appSettings: objOrFn?.appSettings,
cleanProjectDir: objOrFn?.cleanProjectDir,
})
}
await tronApp.initialise(options)
} else {
await tronApp.initialise('')
}
// We need to patch this because addInitScript will bind too late in our
// electron tests, never running. We need to call reload() after each call
// to guarantee it runs.
const oldContextAddInitScript = tronApp.context.addInitScript
tronApp.context.addInitScript = async function (a, b) {
// @ts-ignore pretty sure way out of tsc's type checking capabilities.
// This code works perfectly fine.
await oldContextAddInitScript.apply(this, [a, b])
await tronApp.page.reload()
}
// No idea why we mix and match page and context's addInitScript but we do
const oldPageAddInitScript = tronApp.page.addInitScript
tronApp.page.addInitScript = async function (a: any, b: any) {
// @ts-ignore pretty sure way out of tsc's type checking capabilities.
// This code works perfectly fine.
await oldPageAddInitScript.apply(this, [a, b])
await tronApp.page.reload()
}
// Create a consistent way to resize the page across electron and web.
// (lee) I had to do everything in the book to make electron change its
// damn window size. I succeeded in making it consistently and reliably
// do it after a whole afternoon.
tronApp.page.setBodyDimensions = async function (dims: {
width: number
height: number
}) {
await tronApp.page.setViewportSize(dims)
if (!(tronApp instanceof AuthenticatedTronApp)) {
return return
} }
await tronApp.electronApp?.evaluateHandle(async ({ app }, dims) => { await use(electronZooInstance)
// @ts-ignore sorry jon but see comment in main.ts why this is ignored
await app.resizeWindow(dims.width, dims.height)
}, dims)
return tronApp.page.evaluate(
async (dims: { width: number; height: number }) => {
await window.electron.resizeWindow(dims.width, dims.height)
window.document.body.style.width = dims.width + 'px'
window.document.body.style.height = dims.height + 'px'
window.document.documentElement.style.width = dims.width + 'px'
window.document.documentElement.style.height = dims.height + 'px'
}, },
dims })
)
}
await tronApp.page.setBodyDimensions(tronApp.viewPortSize) const test = playwrightTestFnWithFixtures_.extend<Fixtures>(
fixturesBasedOnProcessEnvPlatform
)
// We need to expose this in order for some tests that require folder export { test }
// creation. Before they used to do this by their own electronSetup({...})
// calls.
if (tronApp instanceof AuthenticatedTronApp) {
tronApp.context.folderSetupFn = async function (fn) {
return fn(tronApp.dir)
.then(() => tronApp.page.reload())
.then(() => ({
dir: tronApp.dir,
}))
}
}
if (!firstUrl) {
await tronApp.page.getByText('Your Projects').count()
firstUrl = tronApp.page.url()
}
// Due to the app controlling its own window context we need to inject new
// options and context here.
// NOTE TO LEE: Seems to destroy page context when calling an electron loadURL.
// await tronApp.electronApp.evaluate(({ app }) => {
// return app.reuseWindowForTest();
// });
await tronApp.electronApp?.evaluate(({ app }, projectDirName) => {
// @ts-ignore can't declaration merge see main.ts
app.testProperty['TEST_SETTINGS_FILE_KEY'] = projectDirName
}, tronApp.dir)
// Always start at the root view
await tronApp.page.goto(firstUrl)
// Force a hard reload, destroying the stream and other state
await tronApp.page.reload()
// tsc aint smart enough to know this'll never be undefined
// but I dont blame it, the logic to know is complex
if (fn) {
await fn(
{
context: tronApp.context,
page: tronApp.page,
electronApp:
tronApp instanceof AuthenticatedTronApp
? tronApp.electronApp
: undefined,
...fixtures,
request,
playwright,
browser,
acceptDownloads,
bypassCSP,
colorScheme,
clientCertificates,
deviceScaleFactor,
extraHTTPHeaders,
geolocation,
hasTouch,
httpCredentials,
ignoreHTTPSErrors,
isMobile,
javaScriptEnabled,
locale,
offline,
permissions,
proxy,
storageState,
timezoneId,
userAgent,
viewport,
baseURL,
contextOptions,
actionTimeout,
navigationTimeout,
serviceWorkers,
testIdAttribute,
browserName,
defaultBrowserType,
headless,
channel,
launchOptions,
connectOptions,
screenshot,
trace,
video,
},
testInfo
)
}
testInfo.tronApp =
tronApp instanceof AuthenticatedTronApp ? tronApp : undefined
}
)
}
type ZooTest = typeof test
test.describe = pwTestFnWithFixtures.describe
test.beforeEach = pwTestFnWithFixtures.beforeEach
test.afterEach = pwTestFnWithFixtures.afterEach
test.step = pwTestFnWithFixtures.step
test.skip = pwTestFnWithFixtures.skip
test.setTimeout = pwTestFnWithFixtures.setTimeout
test.fixme = pwTestFnWithFixtures.fixme as unknown as ZooTest
test.only = pwTestFnWithFixtures.only
test.fail = pwTestFnWithFixtures.fail
test.slow = pwTestFnWithFixtures.slow
test.beforeAll = pwTestFnWithFixtures.beforeAll
test.afterAll = pwTestFnWithFixtures.afterAll
test.use = pwTestFnWithFixtures.use
test.expect = pwTestFnWithFixtures.expect
test.extend = pwTestFnWithFixtures.extend
test.info = pwTestFnWithFixtures.info

View File

@ -106,7 +106,7 @@
"files:flip-to-nightly:windows": "./scripts/flip-files-to-nightly.ps1", "files:flip-to-nightly:windows": "./scripts/flip-files-to-nightly.ps1",
"files:invalidate-bucket": "./scripts/invalidate-files-bucket.sh", "files:invalidate-bucket": "./scripts/invalidate-files-bucket.sh",
"files:invalidate-bucket:nightly": "./scripts/invalidate-files-bucket.sh --nightly", "files:invalidate-bucket:nightly": "./scripts/invalidate-files-bucket.sh --nightly",
"postinstall": "./node_modules/.bin/electron-rebuild", "postinstall": "yarn --cwd ./rust/kcl-language-server --modules-folder node_modules install && ./node_modules/.bin/electron-rebuild",
"make:dev": "make dev", "make:dev": "make dev",
"generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts", "generate:machine-api": "npx openapi-typescript ./openapi/machine-api.json -o src/lib/machine-api.d.ts",
"generate:samples-manifest": "cd public/kcl-samples && node generate-manifest.js", "generate:samples-manifest": "cd public/kcl-samples && node generate-manifest.js",

View File

@ -136,6 +136,7 @@ export default function Gizmo() {
<div <div
ref={wrapperRef} ref={wrapperRef}
aria-label="View orientation gizmo" aria-label="View orientation gizmo"
data-testid={`gizmo${disableOrbitRef.current ? '-disabled' : ''}`}
className="grid place-content-center rounded-full overflow-hidden border border-solid border-primary/50 pointer-events-auto bg-chalkboard-10/70 dark:bg-chalkboard-100/80 backdrop-blur-sm" className="grid place-content-center rounded-full overflow-hidden border border-solid border-primary/50 pointer-events-auto bg-chalkboard-10/70 dark:bg-chalkboard-100/80 backdrop-blur-sm"
> >
<canvas ref={canvasRef} /> <canvas ref={canvasRef} />

View File

@ -1914,7 +1914,7 @@ export class EngineCommandManager extends EventTarget {
this.engineConnection?.tearDown(opts) this.engineConnection?.tearDown(opts)
// Our window.tearDown assignment causes this case to happen which is // Our window.engineCommandManager.tearDown assignment causes this case to happen which is
// only really for tests. // only really for tests.
// @ts-ignore // @ts-ignore
} else if (this.engineCommandManager?.engineConnection) { } else if (this.engineCommandManager?.engineConnection) {

View File

@ -405,6 +405,8 @@ export const getAppSettingsFilePath = async () => {
const testSettingsPath = await window.electron.getAppTestProperty( const testSettingsPath = await window.electron.getAppTestProperty(
'TEST_SETTINGS_FILE_KEY' 'TEST_SETTINGS_FILE_KEY'
) )
if (isTestEnv && !testSettingsPath) return SETTINGS_FILE_NAME
const appConfig = await window.electron.getPath('appData') const appConfig = await window.electron.getPath('appData')
const fullPath = isTestEnv const fullPath = isTestEnv
? testSettingsPath ? testSettingsPath

View File

@ -10,9 +10,15 @@ export const codeManager = new CodeManager()
export const engineCommandManager = new EngineCommandManager() export const engineCommandManager = new EngineCommandManager()
declare global {
interface Window {
editorManager: EditorManager
engineCommandManager: EngineCommandManager
}
}
// Accessible for tests mostly // Accessible for tests mostly
// @ts-ignore window.engineCommandManager = engineCommandManager
window.tearDown = engineCommandManager.tearDown
// This needs to be after codeManager is created. // This needs to be after codeManager is created.
export const kclManager = new KclManager(engineCommandManager) export const kclManager = new KclManager(engineCommandManager)
@ -23,12 +29,6 @@ engineCommandManager.camControlsCameraChange = sceneInfra.onCameraChange
export const sceneEntitiesManager = new SceneEntities(engineCommandManager) export const sceneEntitiesManager = new SceneEntities(engineCommandManager)
declare global {
interface Window {
editorManager: EditorManager
}
}
// This needs to be after sceneInfra and engineCommandManager are is created. // This needs to be after sceneInfra and engineCommandManager are is created.
export const editorManager = new EditorManager() export const editorManager = new EditorManager()