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:
@ -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,
|
||||
|
@ -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'
|
||||
|
@ -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 () => {
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
@ -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' }
|
||||
|
@ -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')
|
||||
|
@ -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 }
|
||||
|
@ -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)
|
||||
|
@ -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()
|
||||
|
@ -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(
|
||||
|
@ -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 })
|
||||
|
@ -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'
|
||||
|
@ -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 () => {
|
||||
|
@ -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'
|
||||
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
@ -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
|
||||
)
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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'
|
||||
|
@ -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,
|
||||
|
@ -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'
|
||||
|
@ -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]
|
||||
|
@ -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 }
|
||||
|
@ -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",
|
||||
|
@ -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} />
|
||||
|
@ -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) {
|
||||
|
@ -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
|
||||
|
@ -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()
|
||||
|
||||
|
Reference in New Issue
Block a user