diff --git a/e2e/playwright/basic-sketch.spec.ts b/e2e/playwright/basic-sketch.spec.ts index 0ff50e1f0..578e7e7ef 100644 --- a/e2e/playwright/basic-sketch.spec.ts +++ b/e2e/playwright/basic-sketch.spec.ts @@ -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, diff --git a/e2e/playwright/can-create-sketches-on-all-planes-and-their-back-sides.spec.ts b/e2e/playwright/can-create-sketches-on-all-planes-and-their-back-sides.spec.ts index 3a3aaedb8..218cd0e59 100644 --- a/e2e/playwright/can-create-sketches-on-all-planes-and-their-back-sides.spec.ts +++ b/e2e/playwright/can-create-sketches-on-all-planes-and-their-back-sides.spec.ts @@ -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' diff --git a/e2e/playwright/desktop-export.spec.ts b/e2e/playwright/desktop-export.spec.ts index ae92071ac..76bf36009 100644 --- a/e2e/playwright/desktop-export.spec.ts +++ b/e2e/playwright/desktop-export.spec.ts @@ -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 () => { diff --git a/e2e/playwright/file-tree.spec.ts b/e2e/playwright/file-tree.spec.ts index fef404739..4b28a25c8 100644 --- a/e2e/playwright/file-tree.spec.ts +++ b/e2e/playwright/file-tree.spec.ts @@ -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) }) } ) diff --git a/e2e/playwright/fixtures/cmdBarFixture.ts b/e2e/playwright/fixtures/cmdBarFixture.ts index 865aea548..18800ef9e 100644 --- a/e2e/playwright/fixtures/cmdBarFixture.ts +++ b/e2e/playwright/fixtures/cmdBarFixture.ts @@ -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 => { if (!(await this.page.getByTestId('command-bar-wrapper').isVisible())) { return { stage: 'commandBarClosed' } diff --git a/e2e/playwright/fixtures/editorFixture.ts b/e2e/playwright/fixtures/editorFixture.ts index b8c5e2d82..c4723e4fd 100644 --- a/e2e/playwright/fixtures/editorFixture.ts +++ b/e2e/playwright/fixtures/editorFixture.ts @@ -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') diff --git a/e2e/playwright/fixtures/fixtureSetup.ts b/e2e/playwright/fixtures/fixtureSetup.ts index 90ed51dbe..7e5897fde 100644 --- a/e2e/playwright/fixtures/fixtureSetup.ts +++ b/e2e/playwright/fixtures/fixtureSetup.ts @@ -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 - folderSetupFn?: (projectDirName: string) => Promise - cleanProjectDir?: boolean - appSettings?: DeepPartial - } = { 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) { + 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 } diff --git a/e2e/playwright/fixtures/homePageFixture.ts b/e2e/playwright/fixtures/homePageFixture.ts index 7606eed44..e6caf43a8 100644 --- a/e2e/playwright/fixtures/homePageFixture.ts +++ b/e2e/playwright/fixtures/homePageFixture.ts @@ -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) diff --git a/e2e/playwright/fixtures/sceneFixture.ts b/e2e/playwright/fixtures/sceneFixture.ts index 73ec11345..d7189a672 100644 --- a/e2e/playwright/fixtures/sceneFixture.ts +++ b/e2e/playwright/fixtures/sceneFixture.ts @@ -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 => { 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() diff --git a/e2e/playwright/fixtures/toolbarFixture.ts b/e2e/playwright/fixtures/toolbarFixture.ts index 6ab60f5d3..892a5775e 100644 --- a/e2e/playwright/fixtures/toolbarFixture.ts +++ b/e2e/playwright/fixtures/toolbarFixture.ts @@ -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 + ) => { + await this.startSketchBtn.click() + await cb() + await this.waitUntilSketchingReady() + } + exitSketch = async () => { await this.exitSketchBtn.click() await expect( diff --git a/e2e/playwright/onboarding-tests.spec.ts b/e2e/playwright/onboarding-tests.spec.ts index 6343f6f8e..8be152a44 100644 --- a/e2e/playwright/onboarding-tests.spec.ts +++ b/e2e/playwright/onboarding-tests.spec.ts @@ -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 }) diff --git a/e2e/playwright/point-click.spec.ts b/e2e/playwright/point-click.spec.ts index 139898784..7bb57ce4a 100644 --- a/e2e/playwright/point-click.spec.ts +++ b/e2e/playwright/point-click.spec.ts @@ -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' diff --git a/e2e/playwright/projects.spec.ts b/e2e/playwright/projects.spec.ts index 07397a8b9..e32b582b4 100644 --- a/e2e/playwright/projects.spec.ts +++ b/e2e/playwright/projects.spec.ts @@ -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 () => { diff --git a/e2e/playwright/regression-tests.spec.ts b/e2e/playwright/regression-tests.spec.ts index 6de674d26..af3747f81 100644 --- a/e2e/playwright/regression-tests.spec.ts +++ b/e2e/playwright/regression-tests.spec.ts @@ -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' diff --git a/e2e/playwright/sketch-tests.spec.ts b/e2e/playwright/sketch-tests.spec.ts index f9660dbb3..5ac703464 100644 --- a/e2e/playwright/sketch-tests.spec.ts +++ b/e2e/playwright/sketch-tests.spec.ts @@ -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) }) }) diff --git a/e2e/playwright/snapshot-tests.spec.ts b/e2e/playwright/snapshot-tests.spec.ts index 33e9bf40c..528a86b0f 100644 --- a/e2e/playwright/snapshot-tests.spec.ts +++ b/e2e/playwright/snapshot-tests.spec.ts @@ -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 ) ) diff --git a/e2e/playwright/test-network-and-connection-issues.spec.ts b/e2e/playwright/test-network-and-connection-issues.spec.ts index 0cc837ff9..cc6b5915f 100644 --- a/e2e/playwright/test-network-and-connection-issues.spec.ts +++ b/e2e/playwright/test-network-and-connection-issues.spec.ts @@ -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) diff --git a/e2e/playwright/test-utils.ts b/e2e/playwright/test-utils.ts index a08c6331c..6d10ecff0 100644 --- a/e2e/playwright/test-utils.ts +++ b/e2e/playwright/test-utils.ts @@ -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 => { @@ -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 - cleanProjectDir?: boolean - appSettings?: DeepPartial - 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 } diff --git a/e2e/playwright/testing-segment-overlays.spec.ts b/e2e/playwright/testing-segment-overlays.spec.ts index 52219d3fa..65456d06b 100644 --- a/e2e/playwright/testing-segment-overlays.spec.ts +++ b/e2e/playwright/testing-segment-overlays.spec.ts @@ -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' diff --git a/e2e/playwright/testing-settings.spec.ts b/e2e/playwright/testing-settings.spec.ts index 70babbff6..aed910fcd 100644 --- a/e2e/playwright/testing-settings.spec.ts +++ b/e2e/playwright/testing-settings.spec.ts @@ -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, - }, - async ({ page, homePage }) => { - await page.setBodyDimensions({ width: 1200, height: 500 }) + await tronApp.cleanProjectDir( + TEST_SETTINGS_CORRUPTED as DeepPartial + ) - // 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, diff --git a/e2e/playwright/text-to-cad-tests.spec.ts b/e2e/playwright/text-to-cad-tests.spec.ts index 5e45a034c..0a97c766b 100644 --- a/e2e/playwright/text-to-cad-tests.spec.ts +++ b/e2e/playwright/text-to-cad-tests.spec.ts @@ -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' diff --git a/e2e/playwright/various.spec.ts b/e2e/playwright/various.spec.ts index 60365bd5d..512ba0c7a 100644 --- a/e2e/playwright/various.spec.ts +++ b/e2e/playwright/various.spec.ts @@ -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] diff --git a/e2e/playwright/zoo-test.ts b/e2e/playwright/zoo-test.ts index 317d79d0e..3e3c433da 100644 --- a/e2e/playwright/zoo-test.ts +++ b/e2e/playwright/zoo-test.ts @@ -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 @@ -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 -} +// 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) - -// 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 - -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( + fixturesBasedOnProcessEnvPlatform +) + +export { test } diff --git a/package.json b/package.json index f71c89ebf..ec9b402b7 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/components/Gizmo.tsx b/src/components/Gizmo.tsx index b500b1332..06c681bb6 100644 --- a/src/components/Gizmo.tsx +++ b/src/components/Gizmo.tsx @@ -136,6 +136,7 @@ export default function Gizmo() {
diff --git a/src/lang/std/engineConnection.ts b/src/lang/std/engineConnection.ts index 44a92d073..e4ac5b247 100644 --- a/src/lang/std/engineConnection.ts +++ b/src/lang/std/engineConnection.ts @@ -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) { diff --git a/src/lib/desktop.ts b/src/lib/desktop.ts index a646c15b2..5e252aaa8 100644 --- a/src/lib/desktop.ts +++ b/src/lib/desktop.ts @@ -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 diff --git a/src/lib/singletons.ts b/src/lib/singletons.ts index 4f9481b72..ca9796c95 100644 --- a/src/lib/singletons.ts +++ b/src/lib/singletons.ts @@ -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()