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 {
getUtils,
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 { getUtils } from './test-utils'
import { EngineCommand } from 'lang/std/artifactGraph'

View File

@ -10,7 +10,11 @@ import fsp from 'fs/promises'
test(
'export works on the first try',
{ tag: ['@electron', '@skipLocalEngine'] },
async ({ page, context, scene }, testInfo) => {
async ({ page, context, scene, tronApp }, testInfo) => {
if (!tronApp) {
fail()
}
await context.folderSetupFn(async (dir) => {
const bracketDir = path.join(dir, 'bracket')
await Promise.all([fsp.mkdir(bracketDir, { recursive: true })])
@ -86,7 +90,7 @@ test(
await expect(exportingToastMessage).not.toBeVisible()
const firstFileFullPath = path.resolve(
getPlaywrightDownloadDir(page),
getPlaywrightDownloadDir(tronApp.projectDirName),
exportFileName
)
await test.step('Check the export size', async () => {
@ -165,7 +169,7 @@ test(
]))
const secondFileFullPath = path.resolve(
getPlaywrightDownloadDir(page),
getPlaywrightDownloadDir(tronApp.projectDirName),
exportFileName
)
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 test.step('Postcondition: there are 5 new lee-*.kcl files', async () => {
await expect(
page
.locator('[data-testid="file-pane-scroll-container"] button')
.filter({ hasText: /lee[-]?[0-5]?/ })
).toHaveCount(5)
await expect
.poll(() =>
page
.locator('[data-testid="file-pane-scroll-container"] button')
.filter({ hasText: /lee[-]?[0-5]?/ })
.count()
)
.toEqual(5)
})
}
)

View File

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

View File

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

View File

@ -1,13 +1,31 @@
/* eslint-disable react-hooks/rules-of-hooks */
import type {
BrowserContext,
ElectronApplication,
Fixtures as PlaywrightFixtures,
TestInfo,
Page,
} 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 { join } from 'path'
import fs from 'node:fs'
import path from 'path'
import { CmdBarFixture } from './cmdBarFixture'
import { EditorFixture } from './editorFixture'
import { ToolbarFixture } from './toolbarFixture'
@ -23,7 +41,7 @@ export class AuthenticatedApp {
public readonly testInfo: TestInfo
public readonly viewPortSize = { width: 1200, height: 500 }
public electronApp: undefined | ElectronApplication
public dir: string = ''
public projectDirName: string = ''
constructor(context: BrowserContext, page: Page, testInfo: TestInfo) {
this.context = context
@ -46,7 +64,7 @@ export class AuthenticatedApp {
}
getInputFile = (fileName: string) => {
return fsp.readFile(
join('rust', 'kcl-lib', 'e2e', 'executor', 'inputs', fileName),
path.join('rust', 'kcl-lib', 'e2e', 'executor', 'inputs', fileName),
'utf-8'
)
}
@ -59,101 +77,300 @@ export interface Fixtures {
scene: SceneFixture
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(
browserContext: BrowserContext,
originalPage: Page,
testInfo: TestInfo
) {
this.page = originalPage
this.originalPage = originalPage
this.browserContext = browserContext
// Will be overwritten in the initializer
this.context = browserContext
this.testInfo = testInfo
}
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,
export class ElectronZoo {
public available: boolean = true
public electron!: ElectronApplication
public firstUrl = ''
public viewPortSize = { width: 1200, height: 500 }
public projectDirName = ''
public page!: Page
public context!: BrowserContext
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)
}
window.engineCommandManager.tearDown()
// Keep polling (per js event tick) until state is Disconnected.
const checkDisconnected = () => {
// It's possible we never even created an engineConnection
// e.g. never left Projects view.
if (
window.engineCommandManager?.engineConnection?.state.type ===
'disconnected'
) {
return resolve(undefined)
}
setTimeout(checkDisconnected, 0)
}
checkDisconnected()
})
})
this.page = page
// These assignments "fix" some brokenness in the Playwright Workbench when
// running against electron applications.
// The timeline is still broken but failure screenshots work again.
this.context = context
// TODO: try to get this to work again for screenshots, but it messed with test ends when enabled
// Object.assign(this.browserContext, this.context)
await this.context.tracing.stopChunk({ path: 'trace.zip' })
this.electronApp = electronApp
this.dir = dir
// Only after cleanup we're ready.
this.available = true
}
// Easier to access throughout utils
this.page.dir = dir
async createInstanceIfMissing(testInfo: TestInfo) {
// Create or otherwise clear the folder.
this.projectDirName = testInfo.outputPath('electron-test-projects-dir')
// Setup localStorage, addCookies, reload
await setup(this.context, this.page, this.testInfo)
// We need to expose this in order for some tests that require folder
// creation and some code below.
const that = this
for (const key of unsafeTypedKeys(arg.fixtures)) {
const fixture = arg.fixtures[key]
if (
!fixture ||
fixture instanceof AuthenticatedApp ||
fixture instanceof AuthenticatedTronApp
)
continue
fixture.reConstruct(page)
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',
}
: {}),
...(process.env.PLAYWRIGHT_RECORD_VIDEO
? {
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()
}
close = async () => {
await this.electronApp?.close?.()
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)
}
debugPause = () =>
new Promise(() => {
console.log('UN-RESOLVING PROMISE')
})
}
export const fixtures = {
cmdBar: async ({ page }: { page: Page }, use: any) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
// If yee encounter this, please try to type it.
type FnUse = any
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))
},
editor: async ({ page }: { page: Page }, use: any) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
editor: async ({ page }: { page: Page }, use: FnUse) => {
await use(new EditorFixture(page))
},
toolbar: async ({ page }: { page: Page }, use: any) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
toolbar: async ({ page }: { page: Page }, use: FnUse) => {
await use(new ToolbarFixture(page))
},
scene: async ({ page }: { page: Page }, use: any) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
scene: async ({ page }: { page: Page }, use: FnUse) => {
await use(new SceneFixture(page))
},
homePage: async ({ page }: { page: Page }, use: any) => {
// eslint-disable-next-line react-hooks/rules-of-hooks
homePage: async ({ page }: { page: Page }, use: FnUse) => {
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) {
this.page = page
this.reConstruct(page)
}
reConstruct = (page: Page) => {
this.page = page
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...')
}
createAndGoToProject = async (projectTitle = 'project-$nnn') => {
await this.projectsLoaded()
await this.projectButtonNew.click()
await this.projectTextName.click()
await this.projectTextName.fill(projectTitle)

View File

@ -53,7 +53,12 @@ export class SceneFixture {
constructor(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> => {
const camera = await this.getCameraInfo()
@ -72,17 +77,6 @@ export class SceneFixture {
.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 = (
x: number,
y: number,
@ -253,7 +247,7 @@ export class SceneFixture {
await u.openDebugPanel()
await u.expectCmdLog('[data-message-type="execution-done"]')
await u.clearAndCloseDebugPanel()
await u.closeDebugPanel()
await this.waitForExecutionDone()
await expect(this.startEditSketchBtn).not.toBeDisabled()

View File

@ -37,13 +37,12 @@ export class ToolbarFixture {
featureTreeId = 'feature-tree' as const
/** The pane element for the Feature Tree */
featureTreePane!: Locator
gizmo!: Locator
gizmoDisabled!: Locator
constructor(page: Page) {
this.page = page
this.reConstruct(page)
}
reConstruct = (page: Page) => {
this.page = page
this.extrudeButton = page.getByTestId('extrude')
this.loftButton = page.getByTestId('loft')
this.sweepButton = page.getByTestId('sweep')
@ -67,6 +66,13 @@ export class ToolbarFixture {
this.filePane = page.locator('#files-pane')
this.featureTreePane = page.locator('#feature-tree-pane')
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() {
@ -86,6 +92,18 @@ export class ToolbarFixture {
startSketchPlaneSelection = async () =>
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 () => {
await this.exitSketchBtn.click()
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.
test.describe('Onboarding tests', () => {
test(
'Onboarding code is shown in the editor',
{
appSettings: {
app: {
onboarding_status: '',
},
},
cleanProjectDir: true,
},
async ({ page, homePage }) => {
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
// Test that the onboarding pane loaded
await expect(
page.getByText('Welcome to Modeling App! This')
).toBeVisible()
// Test that the onboarding pane loaded
await expect(
page.getByText('Welcome to Modeling App! This')
).toBeVisible()
// *and* that the code is shown in the editor
await expect(page.locator('.cm-content')).toContainText(
'// Shelf Bracket'
)
// Make sure the model loaded
const XYPlanePoint = { x: 774, y: 116 } as const
const modelColor: [number, number, number] = [45, 45, 45]
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
expect(await u.getGreatestPixDiff(XYPlanePoint, modelColor)).toBeLessThan(
8
)
test('Onboarding code is shown in the editor', async ({
page,
homePage,
tronApp,
}) => {
if (!tronApp) {
fail()
}
)
await tronApp.cleanProjectDir({
app: {
onboarding_status: '',
},
})
const u = await getUtils(page)
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
// Test that the onboarding pane loaded
await expect(page.getByText('Welcome to Modeling App! This')).toBeVisible()
// Test that the onboarding pane loaded
await expect(page.getByText('Welcome to Modeling App! This')).toBeVisible()
// *and* that the code is shown in the editor
await expect(page.locator('.cm-content')).toContainText('// Shelf Bracket')
// Make sure the model loaded
const XYPlanePoint = { x: 774, y: 116 } as const
const modelColor: [number, number, number] = [45, 45, 45]
await page.mouse.move(XYPlanePoint.x, XYPlanePoint.y)
expect(await u.getGreatestPixDiff(XYPlanePoint, modelColor)).toBeLessThan(8)
})
test(
'Desktop: fresh onboarding executes and loads',
{
tag: '@electron',
appSettings: {
},
async ({ page, tronApp }) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: {
onboarding_status: '',
},
},
cleanProjectDir: true,
},
async ({ page }) => {
})
const u = await getUtils(page)
const viewportSize = { width: 1200, height: 500 }
@ -107,223 +103,235 @@ test.describe('Onboarding tests', () => {
}
)
test(
'Code resets after confirmation',
{
cleanProjectDir: true,
},
async ({ context, page, homePage }) => {
const initialCode = `sketch001 = startSketchOn('XZ')`
test('Code resets after confirmation', async ({
context,
page,
homePage,
tronApp,
scene,
cmdBar,
}) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir()
// Load the page up with some code so we see the confirmation warning
// when we go to replay onboarding
await context.addInitScript((code) => {
localStorage.setItem('persistCode', code)
}, initialCode)
const initialCode = `sketch001 = startSketchOn('XZ')`
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
// Load the page up with some code so we see the confirmation warning
// when we go to replay onboarding
await page.addInitScript((code) => {
localStorage.setItem('persistCode', code)
}, initialCode)
// Replay the onboarding
await page.getByRole('link', { name: 'Settings' }).last().click()
const replayButton = page.getByRole('button', {
name: 'Replay onboarding',
})
await expect(replayButton).toBeVisible()
await replayButton.click()
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await scene.connectionEstablished()
// Ensure we see the warning, and that the code has not yet updated
await expect(page.getByText('Would you like to create')).toBeVisible()
await expect(page.locator('.cm-content')).toHaveText(initialCode)
// Replay the onboarding
await page.getByRole('link', { name: 'Settings' }).last().click()
const replayButton = page.getByRole('button', {
name: 'Replay onboarding',
})
await expect(replayButton).toBeVisible()
await replayButton.click()
const nextButton = page.getByTestId('onboarding-next')
// Ensure we see the warning, and that the code has not yet updated
await expect(page.getByText('Would you like to create')).toBeVisible()
await expect(page.locator('.cm-content')).toHaveText(initialCode)
const nextButton = page.getByTestId('onboarding-next')
await nextButton.hover()
await nextButton.click()
// Ensure we see the introduction and that the code has been reset
await expect(page.getByText('Welcome to Modeling App!')).toBeVisible()
await expect(page.locator('.cm-content')).toContainText('// Shelf Bracket')
// 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
// saved to the file system, which we have other tests for.
})
test('Click through each onboarding step and back', async ({
context,
page,
homePage,
tronApp,
}) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: {
onboarding_status: '',
},
})
// Override beforeEach test setup
await context.addInitScript(
async ({ settingsKey, settings }) => {
// Give no initial code, so that the onboarding start is shown immediately
localStorage.setItem('persistCode', '')
localStorage.setItem(settingsKey, settings)
},
{
settingsKey: TEST_SETTINGS_KEY,
settings: settingsToToml({
settings: TEST_SETTINGS_ONBOARDING_START,
}),
}
)
await page.setBodyDimensions({ width: 1200, height: 1080 })
await homePage.goToModelingScene()
// Test that the onboarding pane loaded
await expect(page.getByText('Welcome to Modeling App! This')).toBeVisible()
const nextButton = page.getByTestId('onboarding-next')
const prevButton = page.getByTestId('onboarding-prev')
while ((await nextButton.innerText()) !== 'Finish') {
await nextButton.hover()
await nextButton.click()
// Ensure we see the introduction and that the code has been reset
await expect(page.getByText('Welcome to Modeling App!')).toBeVisible()
await expect(page.locator('.cm-content')).toContainText(
'// Shelf Bracket'
)
// 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
// saved to the file system, which we have other tests for.
}
)
test(
'Click through each onboarding step and back',
{
appSettings: {
app: {
onboarding_status: '',
},
},
},
async ({ context, page, homePage }) => {
// Override beforeEach test setup
await context.addInitScript(
async ({ settingsKey, settings }) => {
// Give no initial code, so that the onboarding start is shown immediately
localStorage.setItem('persistCode', '')
localStorage.setItem(settingsKey, settings)
},
{
settingsKey: TEST_SETTINGS_KEY,
settings: settingsToToml({
settings: TEST_SETTINGS_ONBOARDING_START,
}),
}
)
await page.setBodyDimensions({ width: 1200, height: 1080 })
await homePage.goToModelingScene()
// Test that the onboarding pane loaded
await expect(
page.getByText('Welcome to Modeling App! This')
).toBeVisible()
const nextButton = page.getByTestId('onboarding-next')
const prevButton = page.getByTestId('onboarding-prev')
while ((await nextButton.innerText()) !== 'Finish') {
await nextButton.hover()
await nextButton.click()
}
while ((await prevButton.innerText()) !== 'Dismiss') {
await prevButton.hover()
await prevButton.click()
}
// Dismiss the onboarding
while ((await prevButton.innerText()) !== 'Dismiss') {
await prevButton.hover()
await prevButton.click()
// Test that the onboarding pane is gone
await expect(page.getByTestId('onboarding-content')).not.toBeVisible()
await expect.poll(() => page.url()).not.toContain('/onboarding')
}
)
test(
'Onboarding redirects and code updating',
{
appSettings: {
app: {
onboarding_status: '/export',
},
// Dismiss the onboarding
await prevButton.hover()
await prevButton.click()
// Test that the onboarding pane is gone
await expect(page.getByTestId('onboarding-content')).not.toBeVisible()
await expect.poll(() => page.url()).not.toContain('/onboarding')
})
test('Onboarding redirects and code updating', async ({
context,
page,
homePage,
tronApp,
}) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: {
onboarding_status: '/export',
},
cleanProjectDir: true,
},
async ({ context, page, homePage }) => {
const originalCode = 'sigmaAllow = 15000'
})
// Override beforeEach test setup
await context.addInitScript(
async ({ settingsKey, settings }) => {
// Give some initial code, so we can test that it's cleared
localStorage.setItem('persistCode', originalCode)
localStorage.setItem(settingsKey, settings)
},
{
settingsKey: TEST_SETTINGS_KEY,
settings: settingsToToml({
settings: TEST_SETTINGS_ONBOARDING_EXPORT,
}),
}
)
const originalCode = 'sigmaAllow = 15000'
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
// Test that the redirect happened
await expect.poll(() => page.url()).toContain('/onboarding/export')
// Test that you come back to this page when you refresh
await page.reload()
await expect.poll(() => page.url()).toContain('/onboarding/export')
// Test that the code changes when you advance to the next step
await page.getByTestId('onboarding-next').hover()
await page.getByTestId('onboarding-next').click()
// Test that the onboarding pane loaded
const title = page.locator('[data-testid="onboarding-content"]')
await expect(title).toBeAttached()
await expect(page.locator('.cm-content')).not.toHaveText(originalCode)
// Test that the code is not empty when you click on the next step
await page.locator('[data-testid="onboarding-next"]').hover()
await page.locator('[data-testid="onboarding-next"]').click()
await expect(page.locator('.cm-content')).toHaveText(/.+/)
}
)
test(
'Onboarding code gets reset to demo on Interactive Numbers step',
{
appSettings: {
app: {
onboarding_status: '/parametric-modeling',
},
// Override beforeEach test setup
await context.addInitScript(
async ({ settingsKey, settings }) => {
// Give some initial code, so we can test that it's cleared
localStorage.setItem('persistCode', originalCode)
localStorage.setItem(settingsKey, settings)
},
cleanProjectDir: true,
},
{
settingsKey: TEST_SETTINGS_KEY,
settings: settingsToToml({
settings: TEST_SETTINGS_ONBOARDING_EXPORT,
}),
}
)
async ({ page, homePage }) => {
const u = await getUtils(page)
const badCode = `// This is bad code we shouldn't see`
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await page.setBodyDimensions({ width: 1200, height: 1080 })
await homePage.goToModelingScene()
// Test that the redirect happened
await expect.poll(() => page.url()).toContain('/onboarding/export')
await expect
.poll(() => page.url())
.toContain(onboardingPaths.PARAMETRIC_MODELING)
// Test that you come back to this page when you refresh
await page.reload()
await expect.poll(() => page.url()).toContain('/onboarding/export')
const bracketNoNewLines = bracket.replace(/\n/g, '')
// Test that the code changes when you advance to the next step
await page.getByTestId('onboarding-next').hover()
await page.getByTestId('onboarding-next').click()
// Check the code got reset on load
await expect(page.locator('#code-pane')).toBeVisible()
await expect(u.codeLocator).toHaveText(bracketNoNewLines, {
timeout: 10_000,
})
// Test that the onboarding pane loaded
const title = page.locator('[data-testid="onboarding-content"]')
await expect(title).toBeAttached()
// Mess with the code again
await u.codeLocator.selectText()
await u.codeLocator.fill(badCode)
await expect(u.codeLocator).toHaveText(badCode)
await expect(page.locator('.cm-content')).not.toHaveText(originalCode)
// Click to the next step
await page.locator('[data-testid="onboarding-next"]').hover()
await page.locator('[data-testid="onboarding-next"]').click()
await page.waitForURL('**' + onboardingPaths.INTERACTIVE_NUMBERS, {
waitUntil: 'domcontentloaded',
})
// Test that the code is not empty when you click on the next step
await page.locator('[data-testid="onboarding-next"]').hover()
await page.locator('[data-testid="onboarding-next"]').click()
await expect(page.locator('.cm-content')).toHaveText(/.+/)
})
// Check that the code has been reset
await expect(u.codeLocator).toHaveText(bracketNoNewLines)
test('Onboarding code gets reset to demo on Interactive Numbers step', async ({
page,
homePage,
tronApp,
}) => {
if (!tronApp) {
fail()
}
)
await tronApp.cleanProjectDir({
app: {
onboarding_status: '/parametric-modeling',
},
})
const u = await getUtils(page)
const badCode = `// This is bad code we shouldn't see`
await page.setBodyDimensions({ width: 1200, height: 1080 })
await homePage.goToModelingScene()
await expect
.poll(() => page.url())
.toContain(onboardingPaths.PARAMETRIC_MODELING)
const bracketNoNewLines = bracket.replace(/\n/g, '')
// Check the code got reset on load
await expect(page.locator('#code-pane')).toBeVisible()
await expect(u.codeLocator).toHaveText(bracketNoNewLines, {
timeout: 10_000,
})
// Mess with the code again
await u.codeLocator.selectText()
await u.codeLocator.fill(badCode)
await expect(u.codeLocator).toHaveText(badCode)
// Click to the next step
await page.locator('[data-testid="onboarding-next"]').hover()
await page.locator('[data-testid="onboarding-next"]').click()
await page.waitForURL('**' + onboardingPaths.INTERACTIVE_NUMBERS, {
waitUntil: 'domcontentloaded',
})
// Check that the code has been reset
await expect(u.codeLocator).toHaveText(bracketNoNewLines)
})
// (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
// low impact of an avatar not showing I'm changing this to fixme.
test.fixme(
'Avatar text updates depending on image load success',
{
appSettings: {
async ({ context, page, homePage, tronApp }) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: {
onboarding_status: '',
},
},
cleanProjectDir: true,
},
async ({ context, page, homePage }) => {
})
// Override beforeEach test setup
await context.addInitScript(
async ({ settingsKey, settings }) => {
@ -388,15 +396,16 @@ test.describe('Onboarding tests', () => {
test.fixme(
"Avatar text doesn't mention avatar when no avatar",
{
appSettings: {
async ({ context, page, homePage, tronApp }) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: {
onboarding_status: '',
},
},
cleanProjectDir: true,
},
async ({ context, page, homePage }) => {
})
// Override beforeEach test setup
await context.addInitScript(
async ({ settingsKey, settings }) => {
@ -444,15 +453,17 @@ test.describe('Onboarding tests', () => {
test.fixme(
'Restarting onboarding on desktop takes one attempt',
{
appSettings: {
async ({ context, page, tronApp }) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: {
onboarding_status: 'dismissed',
},
},
cleanProjectDir: true,
},
async ({ context, page }) => {
})
await context.folderSetupFn(async (dir) => {
const routerTemplateDir = join(dir, 'router-template-slate')
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 { SceneFixture } from './fixtures/sceneFixture'
import { ToolbarFixture } from './fixtures/toolbarFixture'

View File

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

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 * as fsp from 'fs/promises'
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 path from 'node:path'
import { HomePageFixture } from './fixtures/homePageFixture'
@ -2153,6 +2154,8 @@ extrude001 = extrude(profile003, length = 5)
await page.setBodyDimensions({ width: 1000, height: 500 })
await homePage.goToModelingScene()
await page.waitForTimeout(5000)
await expect(
page.getByRole('button', { name: 'Start Sketch' })
).not.toBeDisabled()
@ -2165,7 +2168,7 @@ extrude001 = extrude(profile003, length = 5)
await page.waitForTimeout(600)
await editor.expectEditor.toContain(`sketch001 = startSketchOn('XZ')`)
await toolbar.exitSketchBtn.click()
await toolbar.exitSketch()
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)
})
})

View File

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

View File

@ -84,7 +84,7 @@ test.describe('Test network and connection issues', () => {
'Engine disconnect & reconnect in sketch mode',
{ tag: '@skipLocalEngine' },
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 u = await getUtils(page)

View File

@ -5,8 +5,9 @@ import {
_electron as electron,
ElectronApplication,
Locator,
Page,
} from '@playwright/test'
import { test, Page } from './zoo-test'
import { test } from './zoo-test'
import { EngineCommand } from 'lang/std/artifactGraph'
import fsp from 'fs/promises'
import fsSync from 'fs'
@ -337,7 +338,7 @@ export const getMovementUtils = (opts: any) => {
async function waitForAuthAndLsp(page: Page) {
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]')
// but that doesn't seem to make it to the console for macos/safari :(
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 bbox = await overlay
.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 angleXOffset = Math.cos(((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
.locator(locator)
.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'),
crushKclCodeIntoOneLineAndThenMaybeSome: async () => {
const code = await page.locator('.cm-content').innerText()
@ -504,7 +505,7 @@ export async function getUtils(page: Page, test_?: typeof test) {
) => {
if (cdpSession === null) {
// 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(
@ -631,7 +632,7 @@ export async function getUtils(page: Page, test_?: typeof test) {
panesOpen: async (paneIds: PaneId[]) => {
return test?.step(`Setting ${paneIds} panes to be open`, async () => {
await page.addInitScript(
({ PERSIST_MODELING_CONTEXT, paneIds }) => {
({ PERSIST_MODELING_CONTEXT, paneIds }: any) => {
localStorage.setItem(
PERSIST_MODELING_CONTEXT,
JSON.stringify({ openPanes: paneIds })
@ -722,14 +723,14 @@ export const makeTemplate: (
const PLAYWRIGHT_DOWNLOAD_DIR = 'downloads-during-playwright'
export const getPlaywrightDownloadDir = (page: Page) => {
return path.resolve(page.dir, PLAYWRIGHT_DOWNLOAD_DIR)
export const getPlaywrightDownloadDir = (rootDir: string) => {
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 })
const downloadDir = getPlaywrightDownloadDir(page)
const downloadDir = getPlaywrightDownloadDir(rootDir)
// Expect there to be at least one file
await expect
@ -756,6 +757,7 @@ export interface Paths {
export const doExport = async (
output: Models['OutputFormat_type'],
rootDir: string,
page: Page,
exportFrom: 'dropdown' | 'sidebarButton' | 'commandBar' = 'dropdown'
): Promise<Paths> => {
@ -836,7 +838,7 @@ export const doExport = async (
// (declared in src/lib/exportSave)
// To remain consistent with our old web tests, we want to move some downloads
// (images) to another directory.
await moveDownloadedFileTo(page, downloadLocation)
await moveDownloadedFileTo(rootDir, downloadLocation)
}
return {
@ -859,12 +861,6 @@ export async function tearDown(page: Page, testInfo: TestInfo) {
downloadThroughput: -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,
@ -936,107 +932,11 @@ let electronApp: ElectronApplication | undefined = undefined
let context: BrowserContext | 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) {
// enabled for chrome for now
if (page.context().browser()?.browserType().name() === 'chromium') {
page.on('pageerror', (exception) => {
// No idea wtf exception is
page.on('pageerror', (exception: any) => {
if (isErrorWhitelisted(exception)) {
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 { LineInputsType } from 'lang/std/sketchcombos'

View File

@ -20,35 +20,40 @@ import { DeepPartial } from 'lib/types'
import { Settings } from '@rust/kcl-lib/bindings/Settings'
test.describe('Testing settings', () => {
test(
'Stored settings are validated and fall back to defaults',
test('Stored settings are validated and fall back to defaults', async ({
page,
homePage,
tronApp,
}) => {
if (!tronApp) {
fail()
}
// Override beforeEach test setup
// with corrupted settings
{
appSettings: TEST_SETTINGS_CORRUPTED as DeepPartial<Settings>,
},
async ({ page, homePage }) => {
await page.setBodyDimensions({ width: 1200, height: 500 })
await tronApp.cleanProjectDir(
TEST_SETTINGS_CORRUPTED as DeepPartial<Settings>
)
// Check the settings were reset
const storedSettings = tomlToSettings(
await page.evaluate(
({ settingsKey }) => localStorage.getItem(settingsKey) || '',
{ settingsKey: TEST_SETTINGS_KEY }
)
await page.setBodyDimensions({ width: 1200, height: 500 })
// Check the settings were reset
const storedSettings = tomlToSettings(
await page.evaluate(
({ settingsKey }) => localStorage.getItem(settingsKey) || '',
{ settingsKey: TEST_SETTINGS_KEY }
)
)
expect(storedSettings.settings?.app?.theme).toBe('dark')
expect(storedSettings.settings?.app?.theme).toBe('dark')
// Check that the invalid settings were changed to good defaults
expect(storedSettings.settings?.modeling?.base_unit).toBe('in')
expect(storedSettings.settings?.modeling?.mouse_controls).toBe('zoo')
expect(storedSettings.settings?.app?.project_directory).toBe('')
expect(storedSettings.settings?.project?.default_project_name).toBe(
'project-$nnn'
)
}
)
// Check that the invalid settings were changed to good defaults
expect(storedSettings.settings?.modeling?.base_unit).toBe('in')
expect(storedSettings.settings?.modeling?.mouse_controls).toBe('zoo')
expect(storedSettings.settings?.app?.project_directory).toBe('')
expect(storedSettings.settings?.project?.default_project_name).toBe(
'project-$nnn'
)
})
// The behavior is actually broken. Parent always takes precedence
test.fixme(
@ -357,8 +362,6 @@ test.describe('Testing settings', () => {
`Load desktop app with no settings file`,
{
tag: '@electron',
// This is what makes no settings file get created
cleanProjectDir: false,
},
async ({ page }, testInfo) => {
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`,
{
tag: '@electron',
appSettings: {
},
async ({ context, page, tronApp }, testInfo) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: {
theme_color: '259',
},
},
},
async ({ context, page }, testInfo) => {
})
await page.setBodyDimensions({ width: 1200, height: 500 })
// Selectors and constants
@ -405,15 +412,20 @@ test.describe('Testing settings', () => {
'user settings reload on external change, on project and modeling view',
{
tag: '@electron',
appSettings: {
},
async ({ context, page, tronApp }, testInfo) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
app: {
// Doesn't matter what you set it to. It will
// default to 264.5
theme_color: '0',
},
},
},
async ({ context, page }, testInfo) => {
})
const { dir: projectDirName } = await context.folderSetupFn(
async () => {}
)
@ -783,128 +795,136 @@ test.describe('Testing settings', () => {
})
})
test(
`Changing system theme preferences (via media query) should update UI and stream`,
{
// Override the settings so that the theme is set to `system`
appSettings: TEST_SETTINGS_DEFAULT_THEME,
},
async ({ page, homePage }) => {
const u = await getUtils(page)
// Selectors and constants
const darkBackgroundCss = 'oklch(0.3012 0 264.5)'
const lightBackgroundCss = 'oklch(0.9911 0 264.5)'
const darkBackgroundColor: [number, number, number] = [27, 27, 27]
const lightBackgroundColor: [number, number, number] = [245, 245, 245]
const streamBackgroundPixelIsColor = async (
color: [number, number, number]
) => {
return u.getGreatestPixDiff({ x: 1000, y: 200 }, color)
}
const toolbar = page.locator('menu').filter({ hasText: 'Start Sketch' })
await test.step(`Test setup`, async () => {
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await u.waitForPageLoad()
await page.waitForTimeout(1000)
await expect(toolbar).toBeVisible()
})
await test.step(`Check the background color is light before`, async () => {
await expect(toolbar).toHaveCSS('background-color', lightBackgroundCss)
await expect
.poll(() => streamBackgroundPixelIsColor(lightBackgroundColor))
.toBeLessThan(15)
})
await test.step(`Change media query preference to dark, emulating dusk with system theme`, async () => {
await page.emulateMedia({ colorScheme: 'dark' })
})
await test.step(`Check the background color is dark after`, async () => {
await expect(toolbar).toHaveCSS('background-color', darkBackgroundCss)
await expect
.poll(() => streamBackgroundPixelIsColor(darkBackgroundColor))
.toBeLessThan(15)
})
test(`Changing system theme preferences (via media query) should update UI and stream`, async ({
page,
homePage,
tronApp,
}) => {
if (!tronApp) {
fail()
}
)
test(
`Turning off "Show debug panel" with debug panel open leaves no phantom panel`,
{
await tronApp.cleanProjectDir({
// Override the settings so that the theme is set to `system`
...TEST_SETTINGS_DEFAULT_THEME,
})
const u = await getUtils(page)
// Selectors and constants
const darkBackgroundCss = 'oklch(0.3012 0 264.5)'
const lightBackgroundCss = 'oklch(0.9911 0 264.5)'
const darkBackgroundColor: [number, number, number] = [27, 27, 27]
const lightBackgroundColor: [number, number, number] = [245, 245, 245]
const streamBackgroundPixelIsColor = async (
color: [number, number, number]
) => {
return u.getGreatestPixDiff({ x: 1000, y: 200 }, color)
}
const toolbar = page.locator('menu').filter({ hasText: 'Start Sketch' })
await test.step(`Test setup`, async () => {
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
await u.waitForPageLoad()
await page.waitForTimeout(1000)
await expect(toolbar).toBeVisible()
})
await test.step(`Check the background color is light before`, async () => {
await expect(toolbar).toHaveCSS('background-color', lightBackgroundCss)
await expect
.poll(() => streamBackgroundPixelIsColor(lightBackgroundColor))
.toBeLessThan(15)
})
await test.step(`Change media query preference to dark, emulating dusk with system theme`, async () => {
await page.emulateMedia({ colorScheme: 'dark' })
})
await test.step(`Check the background color is dark after`, async () => {
await expect(toolbar).toHaveCSS('background-color', darkBackgroundCss)
await expect
.poll(() => streamBackgroundPixelIsColor(darkBackgroundColor))
.toBeLessThan(15)
})
})
test(`Turning off "Show debug panel" with debug panel open leaves no phantom panel`, async ({
context,
page,
homePage,
tronApp,
}) => {
if (!tronApp) {
fail()
}
await tronApp.cleanProjectDir({
// Override beforeEach test setup
// with debug panel open
// but "show debug panel" set to false
appSettings: {
...TEST_SETTINGS,
app: { ...TEST_SETTINGS.app, show_debug_panel: false },
modeling: { ...TEST_SETTINGS.modeling },
},
},
async ({ context, page, homePage }) => {
const u = await getUtils(page)
...TEST_SETTINGS,
app: { ...TEST_SETTINGS.app, show_debug_panel: false },
modeling: { ...TEST_SETTINGS.modeling },
})
await context.addInitScript(async () => {
localStorage.setItem(
'persistModelingContext',
'{"openPanes":["debug"]}'
const u = await getUtils(page)
await context.addInitScript(async () => {
localStorage.setItem('persistModelingContext', '{"openPanes":["debug"]}')
})
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
// Constants and locators
const resizeHandle = page.locator('.sidebar-resize-handles > div.block')
const debugPaneButton = page.getByTestId('debug-pane-button')
const commandsButton = page.getByRole('button', { name: 'Commands' })
const debugPaneOption = page.getByRole('option', {
name: 'Settings · app · show debug panel',
})
async function setShowDebugPanelTo(value: 'On' | 'Off') {
await commandsButton.click()
await debugPaneOption.click()
await page.getByRole('option', { name: value }).click()
await expect(
page.getByText(
`Set show debug panel to "${value === 'On'}" for this project`
)
})
await page.setBodyDimensions({ width: 1200, height: 500 })
await homePage.goToModelingScene()
// Constants and locators
const resizeHandle = page.locator('.sidebar-resize-handles > div.block')
const debugPaneButton = page.getByTestId('debug-pane-button')
const commandsButton = page.getByRole('button', { name: 'Commands' })
const debugPaneOption = page.getByRole('option', {
name: 'Settings · app · show debug panel',
})
async function setShowDebugPanelTo(value: 'On' | 'Off') {
await commandsButton.click()
await debugPaneOption.click()
await page.getByRole('option', { name: value }).click()
await expect(
page.getByText(
`Set show debug panel to "${value === 'On'}" for this project`
)
).toBeVisible()
}
await test.step(`Initial load with corrupted settings`, async () => {
// Check that the debug panel is not visible
await expect(debugPaneButton).not.toBeVisible()
// Check the pane resize handle wrapper is not visible
await expect(resizeHandle).not.toBeVisible()
})
await test.step(`Open code pane to verify we see the resize handles`, async () => {
await u.openKclCodePanel()
await expect(resizeHandle).toBeVisible()
await u.closeKclCodePanel()
})
await test.step(`Turn on debug panel, open it`, async () => {
await setShowDebugPanelTo('On')
await expect(debugPaneButton).toBeVisible()
// We want the logic to clear the phantom panel, so we shouldn't see
// the real panel (and therefore the resize handle) yet
await expect(resizeHandle).not.toBeVisible()
await u.openDebugPanel()
await expect(resizeHandle).toBeVisible()
})
await test.step(`Turn off debug panel setting with it open`, async () => {
await setShowDebugPanelTo('Off')
await expect(debugPaneButton).not.toBeVisible()
await expect(resizeHandle).not.toBeVisible()
})
).toBeVisible()
}
)
await test.step(`Initial load with corrupted settings`, async () => {
// Check that the debug panel is not visible
await expect(debugPaneButton).not.toBeVisible()
// Check the pane resize handle wrapper is not visible
await expect(resizeHandle).not.toBeVisible()
})
await test.step(`Open code pane to verify we see the resize handles`, async () => {
await u.openKclCodePanel()
await expect(resizeHandle).toBeVisible()
await u.closeKclCodePanel()
})
await test.step(`Turn on debug panel, open it`, async () => {
await setShowDebugPanelTo('On')
await expect(debugPaneButton).toBeVisible()
// We want the logic to clear the phantom panel, so we shouldn't see
// the real panel (and therefore the resize handle) yet
await expect(resizeHandle).not.toBeVisible()
await u.openDebugPanel()
await expect(resizeHandle).toBeVisible()
})
await test.step(`Turn off debug panel setting with it open`, async () => {
await setShowDebugPanelTo('Off')
await expect(debugPaneButton).not.toBeVisible()
await expect(resizeHandle).not.toBeVisible()
})
})
test(`Change inline units setting`, async ({
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 { join } from 'path'
import fs from 'fs'

View File

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

View File

@ -1,21 +1,11 @@
import {
test as playwrightTestFn,
TestInfo as TestInfoPlaywright,
BrowserContext as BrowserContextPlaywright,
Page as PagePlaywright,
TestDetails as TestDetailsPlaywright,
PlaywrightTestArgs,
PlaywrightTestOptions,
PlaywrightWorkerArgs,
PlaywrightWorkerOptions,
ElectronApplication,
} from '@playwright/test'
/* eslint-disable react-hooks/rules-of-hooks */
import { test as playwrightTestFn, ElectronApplication } from '@playwright/test'
import {
fixtures,
fixturesBasedOnProcessEnvPlatform,
Fixtures,
AuthenticatedTronApp,
AuthenticatedApp,
ElectronZoo,
} from './fixtures/fixtureSetup'
import { Settings } from '@rust/kcl-lib/bindings/Settings'
@ -23,9 +13,6 @@ import { DeepPartial } from 'lib/types'
export { expect } from '@playwright/test'
declare module '@playwright/test' {
interface TestInfo {
tronApp?: AuthenticatedTronApp
}
interface BrowserContext {
folderSetupFn: (
cb: (dir: string) => Promise<void>
@ -41,288 +28,29 @@ declare module '@playwright/test' {
}
}
export type TestInfo = TestInfoPlaywright
export type BrowserContext = BrowserContextPlaywright
export type Page = PagePlaywright
export type TestDetails = TestDetailsPlaywright & {
cleanProjectDir?: boolean
appSettings?: DeepPartial<Settings>
}
// Each worker spawns a new thread, which will spawn its own ElectronZoo.
// So in some sense there is an implicit pool.
// For example, the variable just beneath this text is reused many times
// *for one worker*.
const electronZooInstance = new ElectronZoo()
// Our custom decorated Zoo test object. Makes it easier to add fixtures, and
// switch between web and electron if needed.
const pwTestFnWithFixtures = playwrightTestFn.extend<Fixtures>(fixtures)
// In JavaScript you cannot replace a function's body only (despite functions
// are themselves objects, which you'd expect a body property or something...)
// 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') {
tronApp = new AuthenticatedApp(context, page, testInfo)
} 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
}
await tronApp.electronApp?.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 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)
// We need to expose this in order for some tests that require folder
// 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
const playwrightTestFnWithFixtures_ = playwrightTestFn.extend<{
tronApp?: ElectronZoo
}>({
tronApp: async ({}, use, testInfo) => {
if (process.env.PLATFORM === 'web') {
await use(undefined)
return
}
)
}
type ZooTest = typeof test
await use(electronZooInstance)
},
})
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
const test = playwrightTestFnWithFixtures_.extend<Fixtures>(
fixturesBasedOnProcessEnvPlatform
)
export { test }

View File

@ -106,7 +106,7 @@
"files:flip-to-nightly:windows": "./scripts/flip-files-to-nightly.ps1",
"files:invalidate-bucket": "./scripts/invalidate-files-bucket.sh",
"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",
"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",

View File

@ -136,6 +136,7 @@ export default function Gizmo() {
<div
ref={wrapperRef}
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"
>
<canvas ref={canvasRef} />

View File

@ -1914,7 +1914,7 @@ export class EngineCommandManager extends EventTarget {
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.
// @ts-ignore
} else if (this.engineCommandManager?.engineConnection) {

View File

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

View File

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