* chore: saving off skeleton * fix: saving skeleton * chore: skeleton for loading projects from project directory path * chore: cleaning up useless state transition to be an on event direct to action state * fix: new structure for web vs desktop vs react machine provider code * chore: saving off skeleton * fix: skeleton logic for react? going to move it from a string to obj.string * fix: trying to prevent error element unmount on global react components. This is bricking JS state * fix: we are so back * chore: implemented navigating to specfic KCL file * chore: implementing renaming project * chore: deleting project * fix: auto fixes * fix: old debug/testing file oops * chore: generic create new file * chore: skeleton for web create file provide * chore: basic machine vitest... need to figure out how to get window.electron implemented in vitest? * chore: save off progress before deleting other project implementation, a few missing features still * chore: trying a different init skeleton? most likely will migrate * chore: first attempt of purging projects context provider * chore: enabling toast for some machine state * chore: enabling more toast success and error * chore: writing read write state to the system io based on the project path * fix: tsc fixes * fix: use file system watcher, navigate to project after creation via the requestProjectName * chore: open project command, hooks vs snapshot context helpers * chore: implemented open and create project for e2e testing. They are hard coded in poor spot for now. * fix: codespell fixes * chore: implementing more project commands * chore: PR improvements for root.tsx * chore: leaving comment about new Router.tsx layout * fix: removing debugging code * fix: rewriting component for readability * fix: improving web initialization * chore: implementing import file from url which is not actually that? * fix: clearing search params on import file from url * fix: fixed two e2e tests, forgot needsReview when making new command * fix: fixing some import from url business logic to pass e2e tests * chore: script for diffing circular deps +/- * fix: formatting * fix: massive fix for circular depsga! * fix: trying to fix some errors and auto fmt * fix: updating deps * fix: removing debugging code * fix: big clean up * fix: more deletion * fix: tsc cleanup * fix: TSC TSC TSC TSC! * fix: typo fix * fix: clear query params on web only, desktop not required * fix: removing unused code * fmt * Bring back `trap` removed in merge * Use explicit types instead of `any`s on arg configs * Add project commands directly to command palette * fix: deleting debugging code, from PR review * fix: this got added back(?) * fix: using referred type * fix: more PR clean up * fix: big block comment for xstate architecture decision * fix: more pr comment fixes * fix: saving off logic, need a big cleanup because I hacked it together to get a POC * fix: extra business? * fix: merge conflict just added them back why dude * fix: more PR comments * fix: big ciruclar deps fix, commandBarActor in appActor * chore: writing e2e test, still need to fix 3 bugs * chore: adding more scenarios * fix: formatting * fix: fixing tsc errors * chore: deleting the old text to cad and using the new application level one, almost there * fix: prompt to edit works * fix: large push to get 1 text to cad command... the usage is a little buggy with delete and navigate within /file * fix: settings for highlight edges now works * chore: adding another e2e test * fix: cleaning up e2e tests and writing more of them * fix: tsc type * chore: more e2e improvements, unique project name in text to cad * chore: e2e tests should be good to go * fix: gotcha comment * fix: enabled web t2c, codespell fixes * fix: fixing merge conflcits?? * fix: t2c is back * Rework home layout to have a sidebar fmt I think * Add two links to the bottom of the sidebar Mostly to visually anchor it * Tweak some style things * update test util whose locator needs to change * tsc and fmt * Stupid heading change broke the dang E2E tests * Make that heading locator a part of the home page fixture * pierremtb/new-snaps-for-frank (#6516) Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com> --------- Co-authored-by: Kevin Nadro <kevin@zoo.dev> Co-authored-by: Kevin Nadro <nadr0@users.noreply.github.com> Co-authored-by: Pierre Jacquier <pierrejacquier39@gmail.com>
404 lines
12 KiB
TypeScript
404 lines
12 KiB
TypeScript
/* eslint-disable react-hooks/rules-of-hooks */
|
|
import type {
|
|
BrowserContext,
|
|
ElectronApplication,
|
|
Page,
|
|
TestInfo,
|
|
} from '@playwright/test'
|
|
import { _electron as electron } from '@playwright/test'
|
|
|
|
import fs from 'node:fs'
|
|
import path from 'path'
|
|
import { SETTINGS_FILE_NAME } from '@src/lib/constants'
|
|
import type { DeepPartial } from '@src/lib/types'
|
|
import fsp from 'fs/promises'
|
|
|
|
import type { Settings } from '@rust/kcl-lib/bindings/Settings'
|
|
|
|
import { CmdBarFixture } from '@e2e/playwright/fixtures/cmdBarFixture'
|
|
import { EditorFixture } from '@e2e/playwright/fixtures/editorFixture'
|
|
import { HomePageFixture } from '@e2e/playwright/fixtures/homePageFixture'
|
|
import { SignInPageFixture } from '@e2e/playwright/fixtures/signInPageFixture'
|
|
import { SceneFixture } from '@e2e/playwright/fixtures/sceneFixture'
|
|
import { ToolbarFixture } from '@e2e/playwright/fixtures/toolbarFixture'
|
|
|
|
import { TEST_SETTINGS } from '@e2e/playwright/storageStates'
|
|
import { getUtils, settingsToToml, setup } from '@e2e/playwright/test-utils'
|
|
|
|
export class AuthenticatedApp {
|
|
public readonly page: Page
|
|
public readonly context: BrowserContext
|
|
public readonly testInfo: TestInfo
|
|
public readonly viewPortSize = { width: 1200, height: 500 }
|
|
public electronApp: undefined | ElectronApplication
|
|
public projectDirName: string = ''
|
|
|
|
constructor(context: BrowserContext, page: Page, testInfo: TestInfo) {
|
|
this.context = context
|
|
this.page = page
|
|
this.testInfo = testInfo
|
|
}
|
|
|
|
async initialise(code = '') {
|
|
const testDir = this.testInfo.outputPath('electron-test-projects-dir')
|
|
await setup(this.context, this.page, testDir, this.testInfo)
|
|
const u = await getUtils(this.page)
|
|
|
|
await this.page.addInitScript(async (code) => {
|
|
localStorage.setItem('persistCode', code)
|
|
;(window as any).playwrightSkipFilePicker = true
|
|
}, code)
|
|
|
|
await this.page.setViewportSize(this.viewPortSize)
|
|
|
|
await u.waitForAuthSkipAppStart()
|
|
}
|
|
getInputFile = (fileName: string) => {
|
|
return fsp.readFile(
|
|
path.join('rust', 'kcl-lib', 'e2e', 'executor', 'inputs', fileName),
|
|
'utf-8'
|
|
)
|
|
}
|
|
}
|
|
|
|
export interface Fixtures {
|
|
cmdBar: CmdBarFixture
|
|
editor: EditorFixture
|
|
toolbar: ToolbarFixture
|
|
scene: SceneFixture
|
|
homePage: HomePageFixture
|
|
signInPage: SignInPageFixture
|
|
}
|
|
|
|
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() {}
|
|
|
|
// Help remote end by signaling we're done with the connection.
|
|
// If it takes longer than 10s to stop, just resolve.
|
|
async makeAvailableAgain() {
|
|
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 timeA = Date.now()
|
|
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)
|
|
}
|
|
|
|
if (Date.now() - timeA > 3000) {
|
|
return resolve(undefined)
|
|
}
|
|
|
|
setTimeout(checkDisconnected, 1)
|
|
}
|
|
checkDisconnected()
|
|
})
|
|
})
|
|
|
|
await this.context.tracing.stopChunk({ path: 'trace.zip' })
|
|
|
|
// Only after cleanup we're ready.
|
|
this.available = true
|
|
}
|
|
|
|
async createInstanceIfMissing(testInfo: TestInfo) {
|
|
// Create or otherwise clear the folder.
|
|
this.projectDirName = testInfo.outputPath('electron-test-projects-dir')
|
|
|
|
// We need to expose this in order for some tests that require folder
|
|
// creation and some code below.
|
|
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
const that = this
|
|
|
|
const options = {
|
|
args: ['.', '--no-sandbox'],
|
|
env: {
|
|
...process.env,
|
|
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)
|
|
|
|
// Mac takes quite a long time to create the first window in CI.
|
|
// Turns out we can't trust firstWindow() either. So loop.
|
|
let timeoutId: ReturnType<typeof setTimeout>
|
|
const tryToGetWindowPage = () =>
|
|
new Promise((resolve) => {
|
|
const fn = () => {
|
|
this.page = this.electron.windows()[0]
|
|
timeoutId = setTimeout(() => {
|
|
if (this.page) {
|
|
clearTimeout(timeoutId)
|
|
return resolve(undefined)
|
|
}
|
|
fn()
|
|
}, 0)
|
|
}
|
|
fn()
|
|
})
|
|
|
|
await tryToGetWindowPage()
|
|
|
|
this.context = this.electron.context()
|
|
await this.context.tracing.start({ screenshots: true, snapshots: true })
|
|
|
|
// 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()
|
|
}
|
|
|
|
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()
|
|
}
|
|
}
|
|
|
|
await this.context.tracing.startChunk()
|
|
|
|
// THIS IS ABSOLUTELY NECESSARY TO CHANGE THE PROJECT DIRECTORY BETWEEN
|
|
// TESTS BECAUSE OF THE ELECTRON INSTANCE REUSE.
|
|
await this.electron?.evaluate(({ app }, projectDirName) => {
|
|
// @ts-ignore can't declaration merge see main.ts
|
|
app.testProperty['TEST_SETTINGS_FILE_KEY'] = projectDirName
|
|
}, this.projectDirName)
|
|
|
|
await setup(this.context, this.page, this.projectDirName, 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,
|
|
}))
|
|
}
|
|
|
|
if (!this.firstUrl) {
|
|
await this.page.getByRole('heading', { name: '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();
|
|
// });
|
|
|
|
// Always start at the root view
|
|
await this.page.goto(this.firstUrl)
|
|
|
|
// Force a hard reload, destroying the stream and other state
|
|
await this.page.reload()
|
|
}
|
|
|
|
async cleanProjectDir(appSettings?: DeepPartial<Settings>) {
|
|
try {
|
|
if (fs.existsSync(this.projectDirName)) {
|
|
await fsp.rm(this.projectDirName, { recursive: true })
|
|
}
|
|
} catch (_e) {
|
|
console.error(_e)
|
|
}
|
|
|
|
try {
|
|
await fsp.mkdir(this.projectDirName)
|
|
} catch (error: unknown) {
|
|
void error
|
|
// Not a problem if it already exists.
|
|
}
|
|
|
|
const tempSettingsFilePath = path.resolve(
|
|
this.projectDirName,
|
|
'..',
|
|
SETTINGS_FILE_NAME
|
|
)
|
|
|
|
let settingsOverridesToml = ''
|
|
|
|
if (appSettings) {
|
|
settingsOverridesToml = settingsToToml({
|
|
settings: {
|
|
...TEST_SETTINGS,
|
|
...appSettings,
|
|
app: {
|
|
...TEST_SETTINGS.app,
|
|
...appSettings.app,
|
|
},
|
|
project: {
|
|
...TEST_SETTINGS.project,
|
|
directory: this.projectDirName,
|
|
},
|
|
},
|
|
})
|
|
} else {
|
|
settingsOverridesToml = settingsToToml({
|
|
settings: {
|
|
...TEST_SETTINGS,
|
|
app: {
|
|
...TEST_SETTINGS.app,
|
|
},
|
|
project: {
|
|
...TEST_SETTINGS.project,
|
|
directory: this.projectDirName,
|
|
},
|
|
},
|
|
})
|
|
}
|
|
await fsp.writeFile(tempSettingsFilePath, settingsOverridesToml)
|
|
}
|
|
}
|
|
|
|
// If yee encounter this, please try to type it.
|
|
type FnUse = any
|
|
|
|
const fixturesForElectron = {
|
|
page: async (
|
|
{ tronApp }: { tronApp: ElectronZoo },
|
|
use: FnUse,
|
|
testInfo: TestInfo
|
|
) => {
|
|
await use(tronApp.page)
|
|
},
|
|
context: async (
|
|
{ tronApp }: { tronApp: ElectronZoo },
|
|
use: FnUse,
|
|
testInfo: 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: FnUse) => {
|
|
await use(new EditorFixture(page))
|
|
},
|
|
toolbar: async ({ page }: { page: Page }, use: FnUse) => {
|
|
await use(new ToolbarFixture(page))
|
|
},
|
|
scene: async ({ page }: { page: Page }, use: FnUse) => {
|
|
await use(new SceneFixture(page))
|
|
},
|
|
homePage: async ({ page }: { page: Page }, use: FnUse) => {
|
|
await use(new HomePageFixture(page))
|
|
},
|
|
signInPage: async ({ page }: { page: Page }, use: FnUse) => {
|
|
await use(new SignInPageFixture(page))
|
|
},
|
|
}
|
|
|
|
if (process.env.PLATFORM === 'web') {
|
|
Object.assign(fixturesBasedOnProcessEnvPlatform, fixturesForWeb)
|
|
} else {
|
|
Object.assign(fixturesBasedOnProcessEnvPlatform, fixturesForElectron)
|
|
}
|
|
|
|
export { fixturesBasedOnProcessEnvPlatform }
|