diff --git a/e2e/playwright/code-pane-and-errors.spec.ts b/e2e/playwright/code-pane-and-errors.spec.ts index 14fa1c58c..0e8706c8e 100644 --- a/e2e/playwright/code-pane-and-errors.spec.ts +++ b/e2e/playwright/code-pane-and-errors.spec.ts @@ -120,46 +120,45 @@ test.describe('Code pane and errors', () => { await expect(page.locator('.cm-tooltip').first()).toBeVisible() }) - test.fixme('When error is not in view you can click the badge to scroll to it', async ({ - page, - homePage, - context, - }) => { - // Load the app with the working starter code - await context.addInitScript((code) => { - localStorage.setItem('persistCode', code) - }, TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW) + test.fixme( + 'When error is not in view you can click the badge to scroll to it', + async ({ page, homePage, context }) => { + // Load the app with the working starter code + await context.addInitScript((code) => { + localStorage.setItem('persistCode', code) + }, TEST_CODE_LONG_WITH_ERROR_OUT_OF_VIEW) - await page.setBodyDimensions({ width: 1200, height: 500 }) - await homePage.goToModelingScene() + await page.setBodyDimensions({ width: 1200, height: 500 }) + await homePage.goToModelingScene() - await page.waitForTimeout(1000) + await page.waitForTimeout(1000) - // Ensure badge is present - const codePaneButtonHolder = page.locator('#code-button-holder') - await expect(codePaneButtonHolder).toContainText('notification') + // Ensure badge is present + const codePaneButtonHolder = page.locator('#code-button-holder') + await expect(codePaneButtonHolder).toContainText('notification') - // Ensure we have no errors in the gutter, since error out of view. - await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() + // Ensure we have no errors in the gutter, since error out of view. + await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() - // Click the badge. - const badge = page.locator('#code-badge') - await expect(badge).toBeVisible() - await badge.click() + // Click the badge. + const badge = page.locator('#code-badge') + await expect(badge).toBeVisible() + await badge.click() - // Ensure we have an error diagnostic. - await expect(page.locator('.cm-lint-marker-error').first()).toBeVisible() + // Ensure we have an error diagnostic. + await expect(page.locator('.cm-lint-marker-error').first()).toBeVisible() - // Hover over the error to see the error message - await page.hover('.cm-lint-marker-error') - await expect( - page - .getByText( - 'Modeling command failed: [ApiError { error_code: InternalEngine, message: "Solid3D revolve failed: sketch profile must lie entirely on one side of the revolution axis" }]' - ) - .first() - ).toBeVisible() - }) + // Hover over the error to see the error message + await page.hover('.cm-lint-marker-error') + await expect( + page + .getByText( + 'Modeling command failed: [ApiError { error_code: InternalEngine, message: "Solid3D revolve failed: sketch profile must lie entirely on one side of the revolution axis" }]' + ) + .first() + ).toBeVisible() + } + ) test('When error is not in view WITH LINTS you can click the badge to scroll to it', async ({ context, diff --git a/e2e/playwright/editor-tests.spec.ts b/e2e/playwright/editor-tests.spec.ts index 66de23898..f24359511 100644 --- a/e2e/playwright/editor-tests.spec.ts +++ b/e2e/playwright/editor-tests.spec.ts @@ -568,15 +568,14 @@ test.describe('Editor tests', () => { await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() }) - test.fixme('error with 2 source ranges gets 2 diagnostics', async ({ - page, - homePage, - }) => { - const u = await getUtils(page) - await page.addInitScript(async () => { - localStorage.setItem( - 'persistCode', - `length = .750 + test.fixme( + 'error with 2 source ranges gets 2 diagnostics', + async ({ page, homePage }) => { + const u = await getUtils(page) + await page.addInitScript(async () => { + localStorage.setItem( + 'persistCode', + `length = .750 width = 0.500 height = 0.500 dia = 4 @@ -591,52 +590,53 @@ test.describe('Editor tests', () => { return squareHoleSketch } ` - ) - }) - await page.setBodyDimensions({ width: 1000, height: 500 }) + ) + }) + await page.setBodyDimensions({ width: 1000, height: 500 }) - await homePage.goToModelingScene() - await u.waitForPageLoad() - await page.waitForTimeout(1000) + await homePage.goToModelingScene() + await u.waitForPageLoad() + await page.waitForTimeout(1000) - await u.openDebugPanel() - await u.expectCmdLog('[data-message-type="execution-done"]') - await u.closeDebugPanel() + await u.openDebugPanel() + await u.expectCmdLog('[data-message-type="execution-done"]') + await u.closeDebugPanel() - // check no error to begin with - await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() + // check no error to begin with + await expect(page.locator('.cm-lint-marker-error')).not.toBeVisible() - // Click on the bottom of the code editor to add a new line - await u.codeLocator.click() - await page.keyboard.press('ArrowDown') - await page.keyboard.press('ArrowDown') - await page.keyboard.press('ArrowDown') - await page.keyboard.press('ArrowDown') - await page.keyboard.press('ArrowDown') - await page.keyboard.press('ArrowDown') - await page.keyboard.press('ArrowDown') - await page.keyboard.press('ArrowDown') - await page.keyboard.press('ArrowDown') - await page.keyboard.press('ArrowDown') - await page.keyboard.press('ArrowDown') - await page.keyboard.press('ArrowDown') - await page.keyboard.press('ArrowDown') - await page.keyboard.press('Enter') - await page.keyboard.type(`extrusion = startSketchOn('XY') + // Click on the bottom of the code editor to add a new line + await u.codeLocator.click() + await page.keyboard.press('ArrowDown') + await page.keyboard.press('ArrowDown') + await page.keyboard.press('ArrowDown') + await page.keyboard.press('ArrowDown') + await page.keyboard.press('ArrowDown') + await page.keyboard.press('ArrowDown') + await page.keyboard.press('ArrowDown') + await page.keyboard.press('ArrowDown') + await page.keyboard.press('ArrowDown') + await page.keyboard.press('ArrowDown') + await page.keyboard.press('ArrowDown') + await page.keyboard.press('ArrowDown') + await page.keyboard.press('ArrowDown') + await page.keyboard.press('Enter') + await page.keyboard.type(`extrusion = startSketchOn('XY') |> circle({ center: [0, 0], radius: dia/2 }, %) |> hole(squareHole(length, width, height), %) |> extrude(height, %)`) - // error in gutter - await expect(page.locator('.cm-lint-marker-error').first()).toBeVisible() - await page.hover('.cm-lint-marker-error:first-child') - await expect( - page.getByText('Expected 2 arguments, got 3').first() - ).toBeVisible() + // error in gutter + await expect(page.locator('.cm-lint-marker-error').first()).toBeVisible() + await page.hover('.cm-lint-marker-error:first-child') + await expect( + page.getByText('Expected 2 arguments, got 3').first() + ).toBeVisible() - // Make sure there are two diagnostics - await expect(page.locator('.cm-lint-marker-error')).toHaveCount(2) - }) + // Make sure there are two diagnostics + await expect(page.locator('.cm-lint-marker-error')).toHaveCount(2) + } + ) test('if your kcl gets an error from the engine it is inlined', async ({ context, page, diff --git a/e2e/playwright/fixtures/editorFixture.ts b/e2e/playwright/fixtures/editorFixture.ts index d8dd83b39..ff97e0215 100644 --- a/e2e/playwright/fixtures/editorFixture.ts +++ b/e2e/playwright/fixtures/editorFixture.ts @@ -29,7 +29,7 @@ export class EditorFixture { reConstruct = (page: Page) => { this.page = page - this.codeContent = page.locator('.cm-content') + this.codeContent = page.locator('.cm-content[data-language="kcl"]') this.diagnosticsTooltip = page.locator('.cm-tooltip-lint') this.diagnosticsGutterIcon = page.locator('.cm-lint-marker-error') this.activeLine = this.page.locator('.cm-activeLine') diff --git a/e2e/playwright/fixtures/fixtureSetup.ts b/e2e/playwright/fixtures/fixtureSetup.ts index ac3896d6d..634cf70fe 100644 --- a/e2e/playwright/fixtures/fixtureSetup.ts +++ b/e2e/playwright/fixtures/fixtureSetup.ts @@ -79,7 +79,7 @@ export class AuthenticatedTronApp { appSettings?: Partial } = { fixtures: {} } ) { - const { electronApp, page, context, dir } = await setupElectron({ + const { electronApp, page, context, dir, options } = await setupElectron({ testInfo: this.testInfo, folderSetupFn: arg.folderSetupFn, cleanProjectDir: arg.cleanProjectDir, @@ -96,8 +96,6 @@ export class AuthenticatedTronApp { // Setup localStorage, addCookies, reload await setup(this.context, this.page, this.testInfo) - await page.setViewportSize(this.viewPortSize) - for (const key of unsafeTypedKeys(arg.fixtures)) { const fixture = arg.fixtures[key] if ( diff --git a/e2e/playwright/point-click.spec.ts b/e2e/playwright/point-click.spec.ts index 16903d579..1caa6cdc0 100644 --- a/e2e/playwright/point-click.spec.ts +++ b/e2e/playwright/point-click.spec.ts @@ -851,7 +851,9 @@ const shellPointAndClickCapCases = [ ] shellPointAndClickCapCases.forEach(({ shouldPreselect }) => { test(`Shell point-and-click cap (preselected sketches: ${shouldPreselect})`, async ({ - app, + context, + page, + homePage, scene, editor, toolbar, @@ -861,7 +863,11 @@ shellPointAndClickCapCases.forEach(({ shouldPreselect }) => { |> circle({ center = [0, 0], radius = 30 }, %) extrude001 = extrude(30, sketch001) ` - await app.initialise(initialCode) + await context.addInitScript((initialCode) => { + localStorage.setItem('persistCode', initialCode) + }, initialCode) + await page.setBodyDimensions({ width: 1000, height: 500 }) + await homePage.goToModelingScene() // One dumb hardcoded screen pixel value const testPoint = { x: 575, y: 200 } @@ -888,7 +894,7 @@ shellPointAndClickCapCases.forEach(({ shouldPreselect }) => { commandName: 'Shell', }) await clickOnCap() - await app.page.waitForTimeout(500) + await page.waitForTimeout(500) await cmdBar.progressCmdBar() await cmdBar.progressCmdBar() await cmdBar.expectState({ @@ -904,7 +910,7 @@ shellPointAndClickCapCases.forEach(({ shouldPreselect }) => { } else { await test.step(`Preselect the cap`, async () => { await clickOnCap() - await app.page.waitForTimeout(500) + await page.waitForTimeout(500) }) await test.step(`Go through the command bar flow with a preselected face (cap)`, async () => { @@ -936,8 +942,9 @@ shellPointAndClickCapCases.forEach(({ shouldPreselect }) => { }) test('Shell point-and-click wall', async ({ - app, + context, page, + homePage, scene, editor, toolbar, @@ -952,7 +959,11 @@ test('Shell point-and-click wall', async ({ |> close(%) extrude001 = extrude(40, sketch001) ` - await app.initialise(initialCode) + await context.addInitScript((initialCode) => { + localStorage.setItem('persistCode', initialCode) + }, initialCode) + await page.setBodyDimensions({ width: 1000, height: 500 }) + await homePage.goToModelingScene() // One dumb hardcoded screen pixel value const testPoint = { x: 580, y: 180 } @@ -983,7 +994,7 @@ extrude001 = extrude(40, sketch001) await clickOnCap() await page.keyboard.down('Shift') await clickOnWall() - await app.page.waitForTimeout(500) + await page.waitForTimeout(500) await page.keyboard.up('Shift') await cmdBar.progressCmdBar() await cmdBar.progressCmdBar() diff --git a/e2e/playwright/projects.spec.ts b/e2e/playwright/projects.spec.ts index 6e7669941..cab24566d 100644 --- a/e2e/playwright/projects.spec.ts +++ b/e2e/playwright/projects.spec.ts @@ -115,21 +115,21 @@ test( ) test( - 'open a file in a project works and renders, open another file in different project with errors, it should clear the scene', + 'yyyyyyyyy open a file in a project works and renders, open another file in different project with errors, it should clear the scene', { tag: '@electron' }, async ({ context, page }, testInfo) => { - await context.folderSetupFn(async (dir) => { - const bracketDir = join(dir, 'bracket') + await context.folderSetupFn(async (dir) => { + const bracketDir = path.join(dir, 'bracket') await fsp.mkdir(bracketDir, { recursive: true }) await fsp.copyFile( executorInputPath('focusrite_scarlett_mounting_braket.kcl'), - join(bracketDir, 'main.kcl') + path.join(bracketDir, 'main.kcl') ) - const errorDir = join(dir, 'broken-code') + const errorDir = path.join(dir, 'broken-code') await fsp.mkdir(errorDir, { recursive: true }) await fsp.copyFile( executorInputPath('broken-code-test.kcl'), - join(errorDir, 'main.kcl') + path.join(errorDir, 'main.kcl') ) }) @@ -199,19 +199,19 @@ test( ) test( - 'open a file in a project works and renders, open another file in different project that is empty, it should clear the scene', + 'aaayyyyyyyy open a file in a project works and renders, open another file in different project that is empty, it should clear the scene', { tag: '@electron' }, async ({ context, page }, testInfo) => { await context.folderSetupFn(async (dir) => { - const bracketDir = join(dir, 'bracket') + const bracketDir = path.join(dir, 'bracket') await fsp.mkdir(bracketDir, { recursive: true }) await fsp.copyFile( executorInputPath('focusrite_scarlett_mounting_braket.kcl'), - join(bracketDir, 'main.kcl') + path.join(bracketDir, 'main.kcl') ) - const emptyDir = join(dir, 'empty') + const emptyDir = path.join(dir, 'empty') await fsp.mkdir(emptyDir, { recursive: true }) - await fsp.writeFile(join(emptyDir, 'main.kcl'), '') + await fsp.writeFile(path.join(emptyDir, 'main.kcl'), '') }) await page.setBodyDimensions({ width: 1200, height: 500 }) @@ -276,18 +276,18 @@ test( ) test( - 'open a file in a project works and renders, open empty file, it should clear the scene', + 'nooooooooooooo open a file in a project works and renders, open empty file, it should clear the scene', { tag: '@electron' }, - async ({ browserName }, testInfo) => { + async ({ context, page }, testInfo) => { await context.folderSetupFn(async (dir) => { - const bracketDir = join(dir, 'bracket') + const bracketDir = path.join(dir, 'bracket') await fsp.mkdir(bracketDir, { recursive: true }) await fsp.copyFile( executorInputPath('focusrite_scarlett_mounting_braket.kcl'), - join(bracketDir, 'main.kcl') + path.join(bracketDir, 'main.kcl') ) - await fsp.writeFile(join(bracketDir, 'empty.kcl'), '') + await fsp.writeFile(path.join(bracketDir, 'empty.kcl'), '') }) await page.setBodyDimensions({ width: 1200, height: 500 }) @@ -297,17 +297,13 @@ test( const pointOnModel = { x: 630, y: 280 } + await test.step('Opening the bracket project should load the stream', async () => { // expect to see the text bracket await expect(page.getByText('bracket')).toBeVisible() await page.getByText('bracket').click() - - await expect(page.getByTestId('loading')).toBeAttached() - await expect(page.getByTestId('loading')).not.toBeAttached({ - timeout: 20_000, - }) - + await expect( page.getByRole('button', { name: 'Start Sketch' }) ).toBeEnabled({ @@ -347,27 +343,25 @@ test( ) test( - 'open a file in a project works and renders, open another file in the same project with errors, it should clear the scene', + 'xxxxx open a file in a project works and renders, open another file in the same project with errors, it should clear the scene', { tag: '@electron' }, - async ({ browserName }, testInfo) => { - await context.folderSetupFn(async (dir) => { - const bracketDir = join(dir, 'bracket') + async ({ context, page }, testInfo) => { + const { dir } = await context.folderSetupFn(async (dir) => { + const bracketDir = path.join(dir, 'bracket') await fsp.mkdir(bracketDir, { recursive: true }) await fsp.copyFile( executorInputPath('focusrite_scarlett_mounting_braket.kcl'), - join(bracketDir, 'main.kcl') + path.join(bracketDir, 'main.kcl') ) await fsp.copyFile( executorInputPath('broken-code-test.kcl'), - join(bracketDir, 'broken-code-test.kcl') + path.join(bracketDir, 'broken-code-test.kcl') ) }) await page.setBodyDimensions({ width: 1200, height: 500 }) const u = await getUtils(page) - page.on('console', console.log) - const pointOnModel = { x: 630, y: 280 } await test.step('Opening the bracket project should load the stream', async () => { @@ -399,7 +393,7 @@ test( // open the file pane. await page.getByTestId('files-pane-button').click() - // OPen the other file. + // Open the other file. const file = page.getByRole('button', { name: 'broken-code-test.kcl' }) await expect(file).toBeVisible() @@ -997,7 +991,7 @@ test.describe(`Project management commands`, () => { test( 'File in the file pane should open with a single click', { tag: '@electron' }, - async ({ context, page }, testInfo) => { + async ({ context, homePage, page }, testInfo) => { const projectName = 'router-template-slate' await context.folderSetupFn(async (dir) => { await fsp.mkdir(`${dir}/${projectName}`, { recursive: true }) @@ -1010,16 +1004,14 @@ test( `${dir}/${projectName}/otherThingToClickOn.kcl` ) }) + const u = await getUtils(page) await page.setBodyDimensions({ width: 1200, height: 500 }) page.on('console', console.log) await page.getByText(projectName).click() - await expect(page.getByTestId('loading')).toBeAttached() - await expect(page.getByTestId('loading')).not.toBeAttached({ - timeout: 20_000, - }) + await u.waitForPageLoad() await expect(u.codeLocator).toContainText('routerDiameter') await expect(u.codeLocator).toContainText('templateGap') diff --git a/e2e/playwright/snapshot-tests.spec.ts b/e2e/playwright/snapshot-tests.spec.ts index 8b8349714..1610c4d4f 100644 --- a/e2e/playwright/snapshot-tests.spec.ts +++ b/e2e/playwright/snapshot-tests.spec.ts @@ -47,7 +47,6 @@ test.beforeEach(async ({ page }) => { test.setTimeout(60_000) - // We test this end to end already - getting this to work on web just to take // a snapshot of it feels weird. I'd rather our regular tests fail. // The primary failure is doExport now relies on the filesystem. We can follow diff --git a/e2e/playwright/test-utils.ts b/e2e/playwright/test-utils.ts index 45c874256..e94687961 100644 --- a/e2e/playwright/test-utils.ts +++ b/e2e/playwright/test-utils.ts @@ -28,6 +28,107 @@ import { isErrorWhitelisted } from './lib/console-error-whitelist' import { isArray } from 'lib/utils' import { reportRejection } from 'lib/trap' +// The below is copied from playwright-core because it exports none of them :( +import { Env, BrowserContextOptions } from 'playwright-core' +import type * as channels from '@protocol/channels'; + +// Copied from playwright-core +function envObjectToArray(env: Env): { name: string, value: string }[] { + const result: { name: string, value: string }[] = []; + for (const name in env) { + if (!Object.is(env[name], undefined)) + result.push({ name, value: String(env[name]) }); + } + return result; +} + +// Copied from playwright-core +export async function toClientCertificatesProtocol(certs?: BrowserContextOptions['clientCertificates']): Promise { + if (!certs) + return undefined; + + const bufferizeContent = async (value?: Buffer, path?: string): Promise => { + if (value) + return value; + if (path) + return await fs.promises.readFile(path); + }; + + return await Promise.all(certs.map(async cert => ({ + origin: cert.origin, + cert: await bufferizeContent(cert.cert, cert.certPath), + key: await bufferizeContent(cert.key, cert.keyPath), + pfx: await bufferizeContent(cert.pfx, cert.pfxPath), + passphrase: cert.passphrase, + }))); +} + +// Copied from playwright-core +function toAcceptDownloadsProtocol(acceptDownloads?: boolean) { + if (acceptDownloads === undefined) + return undefined; + if (acceptDownloads) + return 'accept'; + return 'deny'; +} + +// Copied from playwright-core +function prepareRecordHarOptions(options: BrowserContextOptions['recordHar']): channels.RecordHarOptions | undefined { + if (!options) + return; + return { + path: options.path, + content: options.content || (options.omitContent ? 'omit' : undefined), + urlGlob: isString(options.urlFilter) ? options.urlFilter : undefined, + urlRegexSource: isRegExp(options.urlFilter) ? options.urlFilter.source : undefined, + urlRegexFlags: isRegExp(options.urlFilter) ? options.urlFilter.flags : undefined, + mode: options.mode + }; +} + +// Copied from playwright-core +async function prepareStorageState(options: BrowserContextOptions): Promise { + if (typeof options.storageState !== 'string') + return options.storageState; + try { + return JSON.parse(await fs.promises.readFile(options.storageState, 'utf8')); + } catch (e) { + rewriteErrorMessage(e, `Error reading storage state from ${options.storageState}:\n` + e.message); + throw e; + } +} + +// Copied from playwright-core +async function prepareBrowserContextParams(options: BrowserContextOptions): Promise { + if (options.videoSize && !options.videosPath) + throw new Error(`"videoSize" option requires "videosPath" to be specified`); + if (options.extraHTTPHeaders) + network.validateHeaders(options.extraHTTPHeaders); + const contextParams: channels.BrowserNewContextParams = { + ...options, + viewport: options.viewport === null ? undefined : options.viewport, + noDefaultViewport: options.viewport === null, + extraHTTPHeaders: options.extraHTTPHeaders ? headersObjectToArray(options.extraHTTPHeaders) : undefined, + storageState: await prepareStorageState(options), + serviceWorkers: options.serviceWorkers, + recordHar: prepareRecordHarOptions(options.recordHar), + colorScheme: options.colorScheme === null ? 'no-override' : options.colorScheme, + reducedMotion: options.reducedMotion === null ? 'no-override' : options.reducedMotion, + forcedColors: options.forcedColors === null ? 'no-override' : options.forcedColors, + acceptDownloads: toAcceptDownloadsProtocol(options.acceptDownloads), + clientCertificates: await toClientCertificatesProtocol(options.clientCertificates), + }; + if (!contextParams.recordVideo && options.videosPath) { + contextParams.recordVideo = { + dir: options.videosPath, + size: options.videoSize + }; + } + if (contextParams.recordVideo && contextParams.recordVideo.dir) + contextParams.recordVideo.dir = path.resolve(process.cwd(), contextParams.recordVideo.dir); + return contextParams; +} + const toNormalizedCode = (text: string) => { return text.replace(/\s+/g, '') } @@ -723,7 +824,7 @@ const moveDownloadedFileTo = async (page: Page, toLocation: string) => { const files = await fsp.readdir(downloadDir) return files.length }) - .toBe(1) + .toBeGreaterThan(0) // Go through the downloads dir and move files to new location const files = await fsp.readdir(downloadDir) @@ -867,10 +968,12 @@ export async function setup( settings, IS_PLAYWRIGHT_KEY, PLAYWRIGHT_TEST_DIR, + PERSIST_MODELING_CONTEXT, }) => { localStorage.clear() localStorage.setItem('TOKEN_PERSIST_KEY', token) localStorage.setItem('persistCode', ``) + localStorage.setItem(PERSIST_MODELING_CONTEXT, JSON.stringify({openPanes: ['code']})) localStorage.setItem(settingsKey, settings) localStorage.setItem(IS_PLAYWRIGHT_KEY, 'true') localStorage.setItem('PLAYWRIGHT_TEST_DIR', PLAYWRIGHT_TEST_DIR) @@ -891,6 +994,7 @@ export async function setup( }), IS_PLAYWRIGHT_KEY, PLAYWRIGHT_TEST_DIR: TEST_SETTINGS.app.projectDirectory, + PERSIST_MODELING_CONTEXT, } ) @@ -909,12 +1013,15 @@ export async function setup( await page.emulateMedia({ reducedMotion: 'reduce' }) // Trigger a navigation, since loading file:// doesn't. - await page.reload() + // await page.reload() } +let electronApp = undefined +let context = undefined +let page = undefined + export async function setupElectron({ testInfo, - folderSetupFn, cleanProjectDir = true, appSettings, }: { @@ -937,7 +1044,7 @@ export async function setupElectron({ await fsp.mkdir(projectDirName) } - const electronApp = await electron.launch({ + const options = { args: ['.', '--no-sandbox'], env: { ...process.env, @@ -947,15 +1054,19 @@ export async function setupElectron({ ...(process.env.ELECTRON_OVERRIDE_DIST_PATH ? { executablePath: process.env.ELECTRON_OVERRIDE_DIST_PATH + 'electron' } : {}), - }) + } - const context = electronApp.context() - const page = await electronApp.firstWindow() + // Do this once and then reuse window on subsequent calls. + if (!electronApp) { + electronApp = await electron.launch(options) + } - page.TEST_SETTINGS_FILE_KEY = projectDirName - - context.on('console', console.log) - page.on('console', console.log) + 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) @@ -985,11 +1096,7 @@ export async function setupElectron({ await fsp.writeFile(tempSettingsFilePath, settingsOverrides) } - await folderSetupFn?.(projectDirName) - - await setup(context, page) - - return { electronApp, page, context, dir: projectDirName } + return { electronApp, page, context, dir: projectDirName, options } } function failOnConsoleErrors(page: Page, testInfo?: TestInfo) { diff --git a/e2e/playwright/testing-constraints.spec.ts b/e2e/playwright/testing-constraints.spec.ts index 1786427cd..0d5a99054 100644 --- a/e2e/playwright/testing-constraints.spec.ts +++ b/e2e/playwright/testing-constraints.spec.ts @@ -62,7 +62,7 @@ test.describe('Testing constraints', () => { page.getByRole('button', { name: 'Exit Sketch' }) ).toBeVisible() - await page.waitForTimeout(500) // wait for animation + await page.waitForTimeout(2500) // wait for animation // Exit sketch await page.mouse.move(startXPx + PUR * 15, 250 - PUR * 10) @@ -668,7 +668,7 @@ test.describe('Testing constraints', () => { }, ] as const for (const { testName, addVariable, value, constraint } of cases) { - test(`${testName}`, async ({ page }) => { + test(`${testName}`, async ({ context, homePage, page }) => { // constants and locators const cmdBarKclInput = page .getByTestId('cmd-bar-arg-value') @@ -698,9 +698,9 @@ part002 = startSketchOn('XZ') ) }) const u = await getUtils(page) - await page.setViewportSize({ width: 1200, height: 500 }) + await page.setBodyDimensions({ width: 1200, height: 500 }) - await u.waitForAuthSkipAppStart() + await homePage.goToModelingScene() await page.getByText('line([74.36, 130.4], %)').click() await page.getByRole('button', { name: 'Edit Sketch' }).click() diff --git a/e2e/playwright/testing-settings.spec.ts b/e2e/playwright/testing-settings.spec.ts index 517b058d6..9d32caef7 100644 --- a/e2e/playwright/testing-settings.spec.ts +++ b/e2e/playwright/testing-settings.spec.ts @@ -162,91 +162,94 @@ test.describe('Testing settings', () => { await expect(hotkey).toHaveText(text) }) - test.fixme('Project and user settings can be reset', async ({ page, homePage }) => { - const u = await getUtils(page) - await test.step(`Setup`, async () => { - await page.setBodyDimensions({ width: 1200, height: 500 }) - await homePage.goToModelingScene() - await u.waitForPageLoad() - await page.waitForTimeout(1000) - }) - - // Selectors and constants - const projectSettingsTab = page.getByRole('radio', { name: 'Project' }) - const userSettingsTab = page.getByRole('radio', { name: 'User' }) - const resetButton = (level: SettingsLevel) => - page.getByRole('button', { - name: `Reset ${level}-level settings`, - }) - const themeColorSetting = page.locator('#themeColor').getByRole('slider') - const settingValues = { - default: '259', - user: '120', - project: '50', - } - const resetToast = (level: SettingsLevel) => - page.getByText(`${level}-level settings were reset`) - - await test.step(`Open the settings modal`, async () => { - await page.getByRole('link', { name: 'Settings' }).last().click() - await expect( - page.getByRole('heading', { name: 'Settings', exact: true }) - ).toBeVisible() - }) - - await test.step('Set up theme color', async () => { - // Verify we're looking at the project-level settings, - // and it's set to default value - await expect(projectSettingsTab).toBeChecked() - await expect(themeColorSetting).toHaveValue(settingValues.default) - - // Set project-level value to 50 - await themeColorSetting.fill(settingValues.project) - - // Set user-level value to 120 - await userSettingsTab.click() - await themeColorSetting.fill(settingValues.user) - await projectSettingsTab.click() - }) - - await test.step('Reset project settings', async () => { - // Click the reset settings button. - await resetButton('project').click() - - await expect(resetToast('project')).toBeVisible() - await expect(resetToast('project')).not.toBeVisible() - - // Verify it is now set to the inherited user value - await expect(themeColorSetting).toHaveValue(settingValues.user) - - await test.step(`Check that the user settings did not change`, async () => { - await userSettingsTab.click() - await expect(themeColorSetting).toHaveValue(settingValues.user) + test.fixme( + 'Project and user settings can be reset', + async ({ page, homePage }) => { + const u = await getUtils(page) + await test.step(`Setup`, async () => { + await page.setBodyDimensions({ width: 1200, height: 500 }) + await homePage.goToModelingScene() + await u.waitForPageLoad() + await page.waitForTimeout(1000) }) - await test.step(`Set project-level again to test the user-level reset`, async () => { - await projectSettingsTab.click() + // Selectors and constants + const projectSettingsTab = page.getByRole('radio', { name: 'Project' }) + const userSettingsTab = page.getByRole('radio', { name: 'User' }) + const resetButton = (level: SettingsLevel) => + page.getByRole('button', { + name: `Reset ${level}-level settings`, + }) + const themeColorSetting = page.locator('#themeColor').getByRole('slider') + const settingValues = { + default: '259', + user: '120', + project: '50', + } + const resetToast = (level: SettingsLevel) => + page.getByText(`${level}-level settings were reset`) + + await test.step(`Open the settings modal`, async () => { + await page.getByRole('link', { name: 'Settings' }).last().click() + await expect( + page.getByRole('heading', { name: 'Settings', exact: true }) + ).toBeVisible() + }) + + await test.step('Set up theme color', async () => { + // Verify we're looking at the project-level settings, + // and it's set to default value + await expect(projectSettingsTab).toBeChecked() + await expect(themeColorSetting).toHaveValue(settingValues.default) + + // Set project-level value to 50 await themeColorSetting.fill(settingValues.project) + + // Set user-level value to 120 await userSettingsTab.click() - }) - }) - - await test.step('Reset user settings', async () => { - // Click the reset settings button. - await resetButton('user').click() - - await expect(resetToast('user')).toBeVisible() - await expect(resetToast('user')).not.toBeVisible() - - // Verify it is now set to the default value - await expect(themeColorSetting).toHaveValue(settingValues.default) - - await test.step(`Check that the project settings did not change`, async () => { + await themeColorSetting.fill(settingValues.user) await projectSettingsTab.click() - await expect(themeColorSetting).toHaveValue(settingValues.project) }) - }) - }) + + await test.step('Reset project settings', async () => { + // Click the reset settings button. + await resetButton('project').click() + + await expect(resetToast('project')).toBeVisible() + await expect(resetToast('project')).not.toBeVisible() + + // Verify it is now set to the inherited user value + await expect(themeColorSetting).toHaveValue(settingValues.user) + + await test.step(`Check that the user settings did not change`, async () => { + await userSettingsTab.click() + await expect(themeColorSetting).toHaveValue(settingValues.user) + }) + + await test.step(`Set project-level again to test the user-level reset`, async () => { + await projectSettingsTab.click() + await themeColorSetting.fill(settingValues.project) + await userSettingsTab.click() + }) + }) + + await test.step('Reset user settings', async () => { + // Click the reset settings button. + await resetButton('user').click() + + await expect(resetToast('user')).toBeVisible() + await expect(resetToast('user')).not.toBeVisible() + + // Verify it is now set to the default value + await expect(themeColorSetting).toHaveValue(settingValues.default) + + await test.step(`Check that the project settings did not change`, async () => { + await projectSettingsTab.click() + await expect(themeColorSetting).toHaveValue(settingValues.project) + }) + }) + } + ) test.fixme( `Project settings override user settings on desktop`, diff --git a/e2e/playwright/zoo-test.ts b/e2e/playwright/zoo-test.ts index dd7583eaf..591e1f5bf 100644 --- a/e2e/playwright/zoo-test.ts +++ b/e2e/playwright/zoo-test.ts @@ -66,6 +66,8 @@ type PWFunction = ( testInfo: TestInfo ) => void | Promise +let firstUrl = '' + // The below error is due to the extreme type spaghetti going on. playwright/ // types/test.d.ts does not export 2 functions (below is one of them) but tsc // is trying to use a interface name it can't see. @@ -213,17 +215,44 @@ export const test = ( ) } + 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 = function (fn) { - return fn(tronApp.dir).then(() => ({ + 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) => { + console.log("ABCDEFGHI", app.testProperty['TEST_SETTINGS_FILE_KEY']) + 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) { diff --git a/playwright.electron.config.ts b/playwright.electron.config.ts index 2dae6d8bb..74af35acc 100644 --- a/playwright.electron.config.ts +++ b/playwright.electron.config.ts @@ -13,7 +13,7 @@ export default defineConfig({ /* Do not retry */ retries: 0, /* Different amount of parallelism on CI and local. */ - workers: 1, + workers: 30, /* Reporter to use. See https://playwright.dev/docs/test-reporters */ reporter: [ ['dot'], diff --git a/src/lib/desktop.ts b/src/lib/desktop.ts index b9f8b971e..0d3cd5544 100644 --- a/src/lib/desktop.ts +++ b/src/lib/desktop.ts @@ -391,7 +391,7 @@ const getAppFolderName = () => { export const getAppSettingsFilePath = async () => { const isTestEnv = window.electron.process.env.IS_PLAYWRIGHT === 'true' - const testSettingsPath = window.electron.process.env.TEST_SETTINGS_FILE_KEY + const testSettingsPath = await window.electron.getAppTestProperty('TEST_SETTINGS_FILE_KEY') const appConfig = await window.electron.getPath('appData') const fullPath = isTestEnv ? testSettingsPath @@ -408,7 +408,7 @@ export const getAppSettingsFilePath = async () => { } const getTokenFilePath = async () => { const isTestEnv = window.electron.process.env.IS_PLAYWRIGHT === 'true' - const testSettingsPath = window.electron.process.env.TEST_SETTINGS_FILE_KEY + const testSettingsPath = await window.electron.getAppTestProperty('TEST_SETTINGS_FILE_KEY') const appConfig = await window.electron.getPath('appData') const fullPath = isTestEnv ? testSettingsPath diff --git a/src/lib/exportSave.ts b/src/lib/exportSave.ts index 0824ec9c6..912de324f 100644 --- a/src/lib/exportSave.ts +++ b/src/lib/exportSave.ts @@ -18,8 +18,9 @@ const save_ = async (file: ModelingAppFile, toastId: string) => { if (window.electron.process.env.IS_PLAYWRIGHT) { // Skip file picker, save to the test dir downloads directory + const testSettingsPath = await window.electron.getAppTestProperty('TEST_SETTINGS_FILE_KEY') const downloadDir = window.electron.join( - window.electron.process.env.TEST_SETTINGS_FILE_KEY, + testSettingsPath, 'downloads-during-playwright' ) await window.electron.mkdir(downloadDir, { recursive: true }) diff --git a/src/main.ts b/src/main.ts index 0984f0c99..eb8e1c8be 100644 --- a/src/main.ts +++ b/src/main.ts @@ -61,8 +61,8 @@ if (process.defaultApp) { // Must be done before ready event. registerStartupListeners() -const createWindow = (filePath?: string): BrowserWindow => { - const newWindow = new BrowserWindow({ +const createWindow = (filePath?: string, reuse?: boolean): BrowserWindow => { + const newWindow = reuse ? mainWindow : new BrowserWindow({ autoHideMenuBar: true, show: false, width: 1800, @@ -110,7 +110,9 @@ const createWindow = (filePath?: string): BrowserWindow => { // Open the DevTools. // mainWindow.webContents.openDevTools() - newWindow.show() + if (!reuse) { + newWindow.show() + } return newWindow } @@ -141,6 +143,12 @@ app.resizeWindow = async (width: number, height: number) => { return mainWindow?.setSize(width, height) } +app.testProperty = {} + +ipcMain.handle('app.testProperty', (event, propertyName) => { + return app.testProperty[propertyName] +}) + ipcMain.handle('app.resizeWindow', (event, data) => { return mainWindow?.setSize(data[0], data[1]) }) diff --git a/src/preload.ts b/src/preload.ts index 793fc9448..f999de820 100644 --- a/src/preload.ts +++ b/src/preload.ts @@ -31,6 +31,7 @@ const onUpdateDownloadStart = ( const onUpdateError = (callback: (value: Error) => void) => ipcRenderer.on('update-error', (_event: any, value) => callback(value)) const appRestart = () => ipcRenderer.invoke('app.restart') +const getAppTestProperty = (propertyName: string) => ipcRenderer.invoke('app.testProperty', propertyName) const isMac = os.platform() === 'darwin' const isWindows = os.platform() === 'win32' @@ -163,14 +164,15 @@ contextBridge.exposeInMainWorld('electron', { isWindows, isLinux, }, + // Use this to access dynamic properties from the node side. + // INTENDED ONLY TO BE USED FOR TESTS. + getAppTestProperty, process: { - // Setter/getter has to be created because - // these are read-only over the boundary. + // These are read-only over the boundary. env: Object.assign( {}, exposeProcessEnvs([ 'NODE_ENV', - 'TEST_SETTINGS_FILE_KEY', 'VITE_KC_API_WS_MODELING_URL', 'VITE_KC_API_BASE_URL', 'VITE_KC_SITE_BASE_URL',