Should exit sketchMode when creating new file in the file tree pane (#3993)
* fix new file sketch mode issue * initial extron app fixture * Add tests for exiting sketch mode on file tree actions * organise files * before all after all clean up * tweak after each * makes typedKeys as unsafe * update mask for draft line snapshots * fix mask * add fix again
@ -1,4 +1,5 @@
 | 
			
		||||
import { test, expect } from '@playwright/test'
 | 
			
		||||
import { _test, _expect } from './playwright-deprecated'
 | 
			
		||||
import { test, expect } from './fixtures/fixtureSetup'
 | 
			
		||||
import * as fsp from 'fs/promises'
 | 
			
		||||
import * as fs from 'fs'
 | 
			
		||||
import {
 | 
			
		||||
@ -11,14 +12,98 @@ import {
 | 
			
		||||
import { join } from 'path'
 | 
			
		||||
import { FILE_EXT } from 'lib/constants'
 | 
			
		||||
 | 
			
		||||
test.beforeEach(async ({ context, page }, testInfo) => {
 | 
			
		||||
_test.beforeEach(async ({ context, page }, testInfo) => {
 | 
			
		||||
  await setup(context, page, testInfo)
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
test.afterEach(async ({ page }, testInfo) => {
 | 
			
		||||
_test.afterEach(async ({ page }, testInfo) => {
 | 
			
		||||
  await tearDown(page, testInfo)
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
test.describe('integrations tests', () => {
 | 
			
		||||
  test(
 | 
			
		||||
    'Creating a new file or switching file while in sketchMode should exit sketchMode',
 | 
			
		||||
    { tag: '@electron' },
 | 
			
		||||
    async ({ tronApp, homePage, scene, editor, toolbar }) => {
 | 
			
		||||
      test.skip(
 | 
			
		||||
        process.platform === 'win32',
 | 
			
		||||
        'windows times out will waiting for the execution indicator?'
 | 
			
		||||
      )
 | 
			
		||||
      await tronApp.initialise({
 | 
			
		||||
        fixtures: { homePage, scene, editor, toolbar },
 | 
			
		||||
        folderSetupFn: async (dir) => {
 | 
			
		||||
          const bracketDir = join(dir, 'test-sample')
 | 
			
		||||
          await fsp.mkdir(bracketDir, { recursive: true })
 | 
			
		||||
          await fsp.copyFile(
 | 
			
		||||
            executorInputPath('e2e-can-sketch-on-chamfer.kcl'),
 | 
			
		||||
            join(bracketDir, 'main.kcl')
 | 
			
		||||
          )
 | 
			
		||||
        },
 | 
			
		||||
      })
 | 
			
		||||
      const [clickObj] = await scene.makeMouseHelpers(600, 300)
 | 
			
		||||
 | 
			
		||||
      await test.step('setup test', async () => {
 | 
			
		||||
        await homePage.expectState({
 | 
			
		||||
          projectCards: [
 | 
			
		||||
            {
 | 
			
		||||
              title: 'test-sample',
 | 
			
		||||
              fileCount: 1,
 | 
			
		||||
              folderCount: 1,
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
          sortBy: 'last-modified-desc',
 | 
			
		||||
        })
 | 
			
		||||
        await homePage.openProject('test-sample')
 | 
			
		||||
        // windows times out here, hence the skip above
 | 
			
		||||
        await scene.waitForExecutionDone()
 | 
			
		||||
      })
 | 
			
		||||
      await test.step('enter sketch mode', async () => {
 | 
			
		||||
        await clickObj()
 | 
			
		||||
        await scene.moveNoWhere()
 | 
			
		||||
        await editor.expectState({
 | 
			
		||||
          activeLines: [
 | 
			
		||||
            '|>startProfileAt([75.8,317.2],%)//[$startCapTag,$EndCapTag]',
 | 
			
		||||
          ],
 | 
			
		||||
          highlightedCode: '',
 | 
			
		||||
          diagnostics: [],
 | 
			
		||||
        })
 | 
			
		||||
        await toolbar.editSketch()
 | 
			
		||||
        await expect(toolbar.exitSketchBtn).toBeVisible()
 | 
			
		||||
      })
 | 
			
		||||
      await test.step('check sketch mode is exited when creating new file', async () => {
 | 
			
		||||
        await toolbar.fileTreeBtn.click()
 | 
			
		||||
        await toolbar.expectFileTreeState(['main.kcl'])
 | 
			
		||||
        await toolbar.createFile({ wait: true })
 | 
			
		||||
 | 
			
		||||
        // check we're out of sketch mode
 | 
			
		||||
        await expect(toolbar.exitSketchBtn).not.toBeVisible()
 | 
			
		||||
        await expect(toolbar.startSketchBtn).toBeVisible()
 | 
			
		||||
      })
 | 
			
		||||
      await test.step('setup for next assertion', async () => {
 | 
			
		||||
        await toolbar.openFile('main.kcl')
 | 
			
		||||
        await clickObj()
 | 
			
		||||
        await scene.moveNoWhere()
 | 
			
		||||
        await editor.expectState({
 | 
			
		||||
          activeLines: [
 | 
			
		||||
            '|>startProfileAt([75.8,317.2],%)//[$startCapTag,$EndCapTag]',
 | 
			
		||||
          ],
 | 
			
		||||
          highlightedCode: '',
 | 
			
		||||
          diagnostics: [],
 | 
			
		||||
        })
 | 
			
		||||
        await toolbar.editSketch()
 | 
			
		||||
        await expect(toolbar.exitSketchBtn).toBeVisible()
 | 
			
		||||
        await toolbar.expectFileTreeState(['main.kcl', 'Untitled.kcl'])
 | 
			
		||||
      })
 | 
			
		||||
      await test.step('check sketch mode is exited when opening a different file', async () => {
 | 
			
		||||
        await toolbar.openFile('untitled.kcl', { wait: false })
 | 
			
		||||
 | 
			
		||||
        // check we're out of sketch mode
 | 
			
		||||
        await expect(toolbar.exitSketchBtn).not.toBeVisible()
 | 
			
		||||
        await expect(toolbar.startSketchBtn).toBeVisible()
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
test.describe('when using the file tree to', () => {
 | 
			
		||||
  const fromFile = 'main.kcl'
 | 
			
		||||
  const toFile = 'hello.kcl'
 | 
			
		||||
@ -26,11 +111,8 @@ test.describe('when using the file tree to', () => {
 | 
			
		||||
  test(
 | 
			
		||||
    `rename ${fromFile} to ${toFile}, and doesn't crash on reload and settings load`,
 | 
			
		||||
    { tag: '@electron' },
 | 
			
		||||
    async ({ browser: _ }, testInfo) => {
 | 
			
		||||
      const { electronApp, page } = await setupElectron({
 | 
			
		||||
        testInfo,
 | 
			
		||||
        folderSetupFn: async () => {},
 | 
			
		||||
      })
 | 
			
		||||
    async ({ browser: _, tronApp }, testInfo) => {
 | 
			
		||||
      await tronApp.initialise()
 | 
			
		||||
 | 
			
		||||
      const {
 | 
			
		||||
        panesOpen,
 | 
			
		||||
@ -38,10 +120,10 @@ test.describe('when using the file tree to', () => {
 | 
			
		||||
        pasteCodeInEditor,
 | 
			
		||||
        renameFile,
 | 
			
		||||
        editorTextMatches,
 | 
			
		||||
      } = await getUtils(page, test)
 | 
			
		||||
      } = await getUtils(tronApp.page, test)
 | 
			
		||||
 | 
			
		||||
      await page.setViewportSize({ width: 1200, height: 500 })
 | 
			
		||||
      page.on('console', console.log)
 | 
			
		||||
      await tronApp.page.setViewportSize({ width: 1200, height: 500 })
 | 
			
		||||
      tronApp.page.on('console', console.log)
 | 
			
		||||
 | 
			
		||||
      await panesOpen(['files', 'code'])
 | 
			
		||||
 | 
			
		||||
@ -55,39 +137,38 @@ test.describe('when using the file tree to', () => {
 | 
			
		||||
      await pasteCodeInEditor(kclCube)
 | 
			
		||||
 | 
			
		||||
      await renameFile(fromFile, toFile)
 | 
			
		||||
      await page.reload()
 | 
			
		||||
      await tronApp.page.reload()
 | 
			
		||||
 | 
			
		||||
      await test.step('Postcondition: editor has same content as before the rename', async () => {
 | 
			
		||||
        await editorTextMatches(kclCube)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await test.step('Postcondition: opening and closing settings works', async () => {
 | 
			
		||||
        const settingsOpenButton = page.getByRole('link', {
 | 
			
		||||
        const settingsOpenButton = tronApp.page.getByRole('link', {
 | 
			
		||||
          name: 'settings Settings',
 | 
			
		||||
        })
 | 
			
		||||
        const settingsCloseButton = page.getByTestId('settings-close-button')
 | 
			
		||||
        const settingsCloseButton = tronApp.page.getByTestId(
 | 
			
		||||
          'settings-close-button'
 | 
			
		||||
        )
 | 
			
		||||
        await settingsOpenButton.click()
 | 
			
		||||
        await settingsCloseButton.click()
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await electronApp.close()
 | 
			
		||||
      await tronApp.close()
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  test(
 | 
			
		||||
    `create many new untitled files they increment their names`,
 | 
			
		||||
    { tag: '@electron' },
 | 
			
		||||
    async ({ browser: _ }, testInfo) => {
 | 
			
		||||
      const { electronApp, page } = await setupElectron({
 | 
			
		||||
        testInfo,
 | 
			
		||||
        folderSetupFn: async () => {},
 | 
			
		||||
      })
 | 
			
		||||
    async ({ browser: _, tronApp }, testInfo) => {
 | 
			
		||||
      await tronApp.initialise()
 | 
			
		||||
 | 
			
		||||
      const { panesOpen, createAndSelectProject, createNewFile } =
 | 
			
		||||
        await getUtils(page, test)
 | 
			
		||||
        await getUtils(tronApp.page, test)
 | 
			
		||||
 | 
			
		||||
      await page.setViewportSize({ width: 1200, height: 500 })
 | 
			
		||||
      page.on('console', console.log)
 | 
			
		||||
      await tronApp.page.setViewportSize({ width: 1200, height: 500 })
 | 
			
		||||
      tronApp.page.on('console', console.log)
 | 
			
		||||
 | 
			
		||||
      await panesOpen(['files'])
 | 
			
		||||
 | 
			
		||||
@ -101,23 +182,21 @@ test.describe('when using the file tree to', () => {
 | 
			
		||||
 | 
			
		||||
      await test.step('Postcondition: there are 5 new Untitled-*.kcl files', async () => {
 | 
			
		||||
        await expect(
 | 
			
		||||
          page
 | 
			
		||||
          tronApp.page
 | 
			
		||||
            .locator('[data-testid="file-pane-scroll-container"] button')
 | 
			
		||||
            .filter({ hasText: /Untitled[-]?[0-5]?/ })
 | 
			
		||||
        ).toHaveCount(5)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await electronApp.close()
 | 
			
		||||
      await tronApp.close()
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  test(
 | 
			
		||||
    'create a new file with the same name as an existing file cancels the operation',
 | 
			
		||||
    { tag: '@electron' },
 | 
			
		||||
    async ({ browser: _ }, testInfo) => {
 | 
			
		||||
      const { electronApp, page } = await setupElectron({
 | 
			
		||||
        testInfo,
 | 
			
		||||
      })
 | 
			
		||||
    async ({ browser: _, tronApp }, testInfo) => {
 | 
			
		||||
      await tronApp.initialise()
 | 
			
		||||
 | 
			
		||||
      const {
 | 
			
		||||
        openKclCodePanel,
 | 
			
		||||
@ -128,10 +207,10 @@ test.describe('when using the file tree to', () => {
 | 
			
		||||
        renameFile,
 | 
			
		||||
        selectFile,
 | 
			
		||||
        editorTextMatches,
 | 
			
		||||
      } = await getUtils(page, test)
 | 
			
		||||
      } = await getUtils(tronApp.page, _test)
 | 
			
		||||
 | 
			
		||||
      await page.setViewportSize({ width: 1200, height: 500 })
 | 
			
		||||
      page.on('console', console.log)
 | 
			
		||||
      await tronApp.page.setViewportSize({ width: 1200, height: 500 })
 | 
			
		||||
      tronApp.page.on('console', console.log)
 | 
			
		||||
 | 
			
		||||
      await createAndSelectProject('project-000')
 | 
			
		||||
      await openKclCodePanel()
 | 
			
		||||
@ -159,25 +238,22 @@ test.describe('when using the file tree to', () => {
 | 
			
		||||
        await selectFile(kcl1)
 | 
			
		||||
        await editorTextMatches(kclCube)
 | 
			
		||||
      })
 | 
			
		||||
      await page.waitForTimeout(500)
 | 
			
		||||
      await tronApp.page.waitForTimeout(500)
 | 
			
		||||
 | 
			
		||||
      await test.step(`Postcondition: ${kcl2} still exists with the original content`, async () => {
 | 
			
		||||
        await selectFile(kcl2)
 | 
			
		||||
        await editorTextMatches(kclCylinder)
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await electronApp.close()
 | 
			
		||||
      await tronApp?.close?.()
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  test(
 | 
			
		||||
    'deleting all files recreates a default main.kcl with no code',
 | 
			
		||||
    { tag: '@electron' },
 | 
			
		||||
    async ({ browser: _ }, testInfo) => {
 | 
			
		||||
      const { electronApp, page } = await setupElectron({
 | 
			
		||||
        testInfo,
 | 
			
		||||
        folderSetupFn: async () => {},
 | 
			
		||||
      })
 | 
			
		||||
    async ({ browser: _, tronApp }, testInfo) => {
 | 
			
		||||
      await tronApp.initialise()
 | 
			
		||||
 | 
			
		||||
      const {
 | 
			
		||||
        panesOpen,
 | 
			
		||||
@ -185,10 +261,10 @@ test.describe('when using the file tree to', () => {
 | 
			
		||||
        pasteCodeInEditor,
 | 
			
		||||
        deleteFile,
 | 
			
		||||
        editorTextMatches,
 | 
			
		||||
      } = await getUtils(page, test)
 | 
			
		||||
      } = await getUtils(tronApp.page, _test)
 | 
			
		||||
 | 
			
		||||
      await page.setViewportSize({ width: 1200, height: 500 })
 | 
			
		||||
      page.on('console', console.log)
 | 
			
		||||
      await tronApp.page.setViewportSize({ width: 1200, height: 500 })
 | 
			
		||||
      tronApp.page.on('console', console.log)
 | 
			
		||||
 | 
			
		||||
      await panesOpen(['files', 'code'])
 | 
			
		||||
 | 
			
		||||
@ -208,7 +284,7 @@ test.describe('when using the file tree to', () => {
 | 
			
		||||
        await editorTextMatches('')
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await electronApp.close()
 | 
			
		||||
      await tronApp.close()
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
@ -217,10 +293,8 @@ test.describe('when using the file tree to', () => {
 | 
			
		||||
    {
 | 
			
		||||
      tag: '@electron',
 | 
			
		||||
    },
 | 
			
		||||
    async ({ browser: _ }, testInfo) => {
 | 
			
		||||
      const { page } = await setupElectron({
 | 
			
		||||
        testInfo,
 | 
			
		||||
      })
 | 
			
		||||
    async ({ browser: _, tronApp }, testInfo) => {
 | 
			
		||||
      await tronApp.initialise()
 | 
			
		||||
 | 
			
		||||
      const {
 | 
			
		||||
        panesOpen,
 | 
			
		||||
@ -230,10 +304,10 @@ test.describe('when using the file tree to', () => {
 | 
			
		||||
        openDebugPanel,
 | 
			
		||||
        closeDebugPanel,
 | 
			
		||||
        expectCmdLog,
 | 
			
		||||
      } = await getUtils(page, test)
 | 
			
		||||
      } = await getUtils(tronApp.page, test)
 | 
			
		||||
 | 
			
		||||
      await page.setViewportSize({ width: 1200, height: 500 })
 | 
			
		||||
      page.on('console', console.log)
 | 
			
		||||
      await tronApp.page.setViewportSize({ width: 1200, height: 500 })
 | 
			
		||||
      tronApp.page.on('console', console.log)
 | 
			
		||||
 | 
			
		||||
      await panesOpen(['files', 'code'])
 | 
			
		||||
      await createAndSelectProject('project-000')
 | 
			
		||||
@ -248,30 +322,30 @@ test.describe('when using the file tree to', () => {
 | 
			
		||||
 | 
			
		||||
      // Create a large lego file
 | 
			
		||||
      await createNewFile('lego')
 | 
			
		||||
      const legoFile = page.getByRole('listitem').filter({
 | 
			
		||||
        has: page.getByRole('button', { name: 'lego.kcl' }),
 | 
			
		||||
      const legoFile = tronApp.page.getByRole('listitem').filter({
 | 
			
		||||
        has: tronApp.page.getByRole('button', { name: 'lego.kcl' }),
 | 
			
		||||
      })
 | 
			
		||||
      await expect(legoFile).toBeVisible({ timeout: 60_000 })
 | 
			
		||||
      await _expect(legoFile).toBeVisible({ timeout: 60_000 })
 | 
			
		||||
      await legoFile.click()
 | 
			
		||||
      const kclLego = await fsp.readFile(
 | 
			
		||||
        'src/wasm-lib/tests/executor/inputs/lego.kcl',
 | 
			
		||||
        'utf-8'
 | 
			
		||||
      )
 | 
			
		||||
      await pasteCodeInEditor(kclLego)
 | 
			
		||||
      const mainFile = page.getByRole('listitem').filter({
 | 
			
		||||
        has: page.getByRole('button', { name: 'main.kcl' }),
 | 
			
		||||
      const mainFile = tronApp.page.getByRole('listitem').filter({
 | 
			
		||||
        has: tronApp.page.getByRole('button', { name: 'main.kcl' }),
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      // Open settings and enable the debug panel
 | 
			
		||||
      await page
 | 
			
		||||
      await tronApp.page
 | 
			
		||||
        .getByRole('link', {
 | 
			
		||||
          name: 'settings Settings',
 | 
			
		||||
        })
 | 
			
		||||
        .click()
 | 
			
		||||
      await page.locator('#showDebugPanel').getByText('OffOn').click()
 | 
			
		||||
      await page.getByTestId('settings-close-button').click()
 | 
			
		||||
      await tronApp.page.locator('#showDebugPanel').getByText('OffOn').click()
 | 
			
		||||
      await tronApp.page.getByTestId('settings-close-button').click()
 | 
			
		||||
 | 
			
		||||
      await test.step('swap between small and large files', async () => {
 | 
			
		||||
      await _test.step('swap between small and large files', async () => {
 | 
			
		||||
        await openDebugPanel()
 | 
			
		||||
        // Previously created a file so we need to start back at main.kcl
 | 
			
		||||
        await mainFile.click()
 | 
			
		||||
@ -283,12 +357,14 @@ test.describe('when using the file tree to', () => {
 | 
			
		||||
        await expectCmdLog('[data-message-type="execution-done"]', 60_000)
 | 
			
		||||
        await closeDebugPanel()
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await tronApp.close()
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
test.describe('Renaming in the file tree', () => {
 | 
			
		||||
  test(
 | 
			
		||||
_test.describe('Renaming in the file tree', () => {
 | 
			
		||||
  _test(
 | 
			
		||||
    'A file you have open',
 | 
			
		||||
    { tag: '@electron' },
 | 
			
		||||
    async ({ browser: _ }, testInfo) => {
 | 
			
		||||
@ -333,56 +409,56 @@ test.describe('Renaming in the file tree', () => {
 | 
			
		||||
      const renameInput = page.getByPlaceholder('fileToRename.kcl')
 | 
			
		||||
      const codeLocator = page.locator('.cm-content')
 | 
			
		||||
 | 
			
		||||
      await test.step('Open project and file pane', async () => {
 | 
			
		||||
        await expect(projectLink).toBeVisible()
 | 
			
		||||
      await _test.step('Open project and file pane', async () => {
 | 
			
		||||
        await _expect(projectLink).toBeVisible()
 | 
			
		||||
        await projectLink.click()
 | 
			
		||||
        await expect(projectMenuButton).toBeVisible()
 | 
			
		||||
        await expect(projectMenuButton).toContainText('main.kcl')
 | 
			
		||||
        await _expect(projectMenuButton).toBeVisible()
 | 
			
		||||
        await _expect(projectMenuButton).toContainText('main.kcl')
 | 
			
		||||
 | 
			
		||||
        await u.openFilePanel()
 | 
			
		||||
        await expect(fileToRename).toBeVisible()
 | 
			
		||||
        expect(checkUnRenamedFS()).toBeTruthy()
 | 
			
		||||
        expect(checkRenamedFS()).toBeFalsy()
 | 
			
		||||
        await _expect(fileToRename).toBeVisible()
 | 
			
		||||
        _expect(checkUnRenamedFS()).toBeTruthy()
 | 
			
		||||
        _expect(checkRenamedFS()).toBeFalsy()
 | 
			
		||||
        await fileToRename.click()
 | 
			
		||||
        await expect(projectMenuButton).toContainText('fileToRename.kcl')
 | 
			
		||||
        await _expect(projectMenuButton).toContainText('fileToRename.kcl')
 | 
			
		||||
        await u.openKclCodePanel()
 | 
			
		||||
        await expect(codeLocator).toContainText('circle(')
 | 
			
		||||
        await _expect(codeLocator).toContainText('circle(')
 | 
			
		||||
        await u.closeKclCodePanel()
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await test.step('Rename the file', async () => {
 | 
			
		||||
      await _test.step('Rename the file', async () => {
 | 
			
		||||
        await fileToRename.click({ button: 'right' })
 | 
			
		||||
        await renameMenuItem.click()
 | 
			
		||||
        await expect(renameInput).toBeVisible()
 | 
			
		||||
        await _expect(renameInput).toBeVisible()
 | 
			
		||||
        await renameInput.fill(newFileName)
 | 
			
		||||
        await page.keyboard.press('Enter')
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await test.step('Verify the file is renamed', async () => {
 | 
			
		||||
        await expect(fileToRename).not.toBeAttached()
 | 
			
		||||
        await expect(renamedFile).toBeVisible()
 | 
			
		||||
        expect(checkUnRenamedFS()).toBeFalsy()
 | 
			
		||||
        expect(checkRenamedFS()).toBeTruthy()
 | 
			
		||||
      await _test.step('Verify the file is renamed', async () => {
 | 
			
		||||
        await _expect(fileToRename).not.toBeAttached()
 | 
			
		||||
        await _expect(renamedFile).toBeVisible()
 | 
			
		||||
        _expect(checkUnRenamedFS()).toBeFalsy()
 | 
			
		||||
        _expect(checkRenamedFS()).toBeTruthy()
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await test.step('Verify we navigated', async () => {
 | 
			
		||||
        await expect(projectMenuButton).toContainText(newFileName + FILE_EXT)
 | 
			
		||||
      await _test.step('Verify we navigated', async () => {
 | 
			
		||||
        await _expect(projectMenuButton).toContainText(newFileName + FILE_EXT)
 | 
			
		||||
        const url = page.url()
 | 
			
		||||
        expect(url).toContain(newFileName)
 | 
			
		||||
        await expect(projectMenuButton).not.toContainText('fileToRename.kcl')
 | 
			
		||||
        await expect(projectMenuButton).not.toContainText('main.kcl')
 | 
			
		||||
        expect(url).not.toContain('fileToRename.kcl')
 | 
			
		||||
        expect(url).not.toContain('main.kcl')
 | 
			
		||||
        _expect(url).toContain(newFileName)
 | 
			
		||||
        await _expect(projectMenuButton).not.toContainText('fileToRename.kcl')
 | 
			
		||||
        await _expect(projectMenuButton).not.toContainText('main.kcl')
 | 
			
		||||
        _expect(url).not.toContain('fileToRename.kcl')
 | 
			
		||||
        _expect(url).not.toContain('main.kcl')
 | 
			
		||||
 | 
			
		||||
        await u.openKclCodePanel()
 | 
			
		||||
        await expect(codeLocator).toContainText('circle(')
 | 
			
		||||
        await _expect(codeLocator).toContainText('circle(')
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await electronApp.close()
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  test(
 | 
			
		||||
  _test(
 | 
			
		||||
    'A file you do not have open',
 | 
			
		||||
    { tag: '@electron' },
 | 
			
		||||
    async ({ browser: _ }, testInfo) => {
 | 
			
		||||
@ -426,54 +502,54 @@ test.describe('Renaming in the file tree', () => {
 | 
			
		||||
      const renameInput = page.getByPlaceholder('fileToRename.kcl')
 | 
			
		||||
      const codeLocator = page.locator('.cm-content')
 | 
			
		||||
 | 
			
		||||
      await test.step('Open project and file pane', async () => {
 | 
			
		||||
        await expect(projectLink).toBeVisible()
 | 
			
		||||
      await _test.step('Open project and file pane', async () => {
 | 
			
		||||
        await _expect(projectLink).toBeVisible()
 | 
			
		||||
        await projectLink.click()
 | 
			
		||||
        await expect(projectMenuButton).toBeVisible()
 | 
			
		||||
        await expect(projectMenuButton).toContainText('main.kcl')
 | 
			
		||||
        await _expect(projectMenuButton).toBeVisible()
 | 
			
		||||
        await _expect(projectMenuButton).toContainText('main.kcl')
 | 
			
		||||
 | 
			
		||||
        await u.openFilePanel()
 | 
			
		||||
        await expect(fileToRename).toBeVisible()
 | 
			
		||||
        expect(checkUnRenamedFS()).toBeTruthy()
 | 
			
		||||
        expect(checkRenamedFS()).toBeFalsy()
 | 
			
		||||
        await _expect(fileToRename).toBeVisible()
 | 
			
		||||
        _expect(checkUnRenamedFS()).toBeTruthy()
 | 
			
		||||
        _expect(checkRenamedFS()).toBeFalsy()
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await test.step('Rename the file', async () => {
 | 
			
		||||
      await _test.step('Rename the file', async () => {
 | 
			
		||||
        await fileToRename.click({ button: 'right' })
 | 
			
		||||
        await renameMenuItem.click()
 | 
			
		||||
        await expect(renameInput).toBeVisible()
 | 
			
		||||
        await _expect(renameInput).toBeVisible()
 | 
			
		||||
        await renameInput.fill(newFileName)
 | 
			
		||||
        await page.keyboard.press('Enter')
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await test.step('Verify the file is renamed', async () => {
 | 
			
		||||
        await expect(fileToRename).not.toBeAttached()
 | 
			
		||||
        await expect(renamedFile).toBeVisible()
 | 
			
		||||
        expect(checkUnRenamedFS()).toBeFalsy()
 | 
			
		||||
        expect(checkRenamedFS()).toBeTruthy()
 | 
			
		||||
      await _test.step('Verify the file is renamed', async () => {
 | 
			
		||||
        await _expect(fileToRename).not.toBeAttached()
 | 
			
		||||
        await _expect(renamedFile).toBeVisible()
 | 
			
		||||
        _expect(checkUnRenamedFS()).toBeFalsy()
 | 
			
		||||
        _expect(checkRenamedFS()).toBeTruthy()
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await test.step('Verify we have not navigated', async () => {
 | 
			
		||||
        await expect(projectMenuButton).toContainText('main.kcl')
 | 
			
		||||
        await expect(projectMenuButton).not.toContainText(
 | 
			
		||||
      await _test.step('Verify we have not navigated', async () => {
 | 
			
		||||
        await _expect(projectMenuButton).toContainText('main.kcl')
 | 
			
		||||
        await _expect(projectMenuButton).not.toContainText(
 | 
			
		||||
          newFileName + FILE_EXT
 | 
			
		||||
        )
 | 
			
		||||
        await expect(projectMenuButton).not.toContainText('fileToRename.kcl')
 | 
			
		||||
        await _expect(projectMenuButton).not.toContainText('fileToRename.kcl')
 | 
			
		||||
 | 
			
		||||
        const url = page.url()
 | 
			
		||||
        expect(url).toContain('main.kcl')
 | 
			
		||||
        expect(url).not.toContain(newFileName)
 | 
			
		||||
        expect(url).not.toContain('fileToRename.kcl')
 | 
			
		||||
        _expect(url).toContain('main.kcl')
 | 
			
		||||
        _expect(url).not.toContain(newFileName)
 | 
			
		||||
        _expect(url).not.toContain('fileToRename.kcl')
 | 
			
		||||
 | 
			
		||||
        await u.openKclCodePanel()
 | 
			
		||||
        await expect(codeLocator).toContainText('fillet(')
 | 
			
		||||
        await _expect(codeLocator).toContainText('fillet(')
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await electronApp.close()
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  test(
 | 
			
		||||
  _test(
 | 
			
		||||
    `A folder you're not inside`,
 | 
			
		||||
    { tag: '@electron' },
 | 
			
		||||
    async ({ browser: _ }, testInfo) => {
 | 
			
		||||
@ -519,48 +595,51 @@ test.describe('Renaming in the file tree', () => {
 | 
			
		||||
        return fs.existsSync(folderPath)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      await test.step('Open project and file pane', async () => {
 | 
			
		||||
        await expect(projectLink).toBeVisible()
 | 
			
		||||
      await _test.step('Open project and file pane', async () => {
 | 
			
		||||
        await _expect(projectLink).toBeVisible()
 | 
			
		||||
        await projectLink.click()
 | 
			
		||||
        await expect(projectMenuButton).toBeVisible()
 | 
			
		||||
        await expect(projectMenuButton).toContainText('main.kcl')
 | 
			
		||||
        await _expect(projectMenuButton).toBeVisible()
 | 
			
		||||
        await _expect(projectMenuButton).toContainText('main.kcl')
 | 
			
		||||
 | 
			
		||||
        const url = page.url()
 | 
			
		||||
        expect(url).toContain('main.kcl')
 | 
			
		||||
        expect(url).not.toContain('folderToRename')
 | 
			
		||||
        _expect(url).toContain('main.kcl')
 | 
			
		||||
        _expect(url).not.toContain('folderToRename')
 | 
			
		||||
 | 
			
		||||
        await u.openFilePanel()
 | 
			
		||||
        await expect(folderToRename).toBeVisible()
 | 
			
		||||
        expect(checkUnRenamedFolderFS()).toBeTruthy()
 | 
			
		||||
        expect(checkRenamedFolderFS()).toBeFalsy()
 | 
			
		||||
        await _expect(folderToRename).toBeVisible()
 | 
			
		||||
        _expect(checkUnRenamedFolderFS()).toBeTruthy()
 | 
			
		||||
        _expect(checkRenamedFolderFS()).toBeFalsy()
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await test.step('Rename the folder', async () => {
 | 
			
		||||
      await _test.step('Rename the folder', async () => {
 | 
			
		||||
        await folderToRename.click({ button: 'right' })
 | 
			
		||||
        await expect(renameMenuItem).toBeVisible()
 | 
			
		||||
        await _expect(renameMenuItem).toBeVisible()
 | 
			
		||||
        await renameMenuItem.click()
 | 
			
		||||
        await expect(renameInput).toBeVisible()
 | 
			
		||||
        await _expect(renameInput).toBeVisible()
 | 
			
		||||
        await renameInput.fill(newFolderName)
 | 
			
		||||
        await page.keyboard.press('Enter')
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await test.step('Verify the folder is renamed, and no navigation occurred', async () => {
 | 
			
		||||
        const url = page.url()
 | 
			
		||||
        expect(url).toContain('main.kcl')
 | 
			
		||||
        expect(url).not.toContain('folderToRename')
 | 
			
		||||
      await _test.step(
 | 
			
		||||
        'Verify the folder is renamed, and no navigation occurred',
 | 
			
		||||
        async () => {
 | 
			
		||||
          const url = page.url()
 | 
			
		||||
          _expect(url).toContain('main.kcl')
 | 
			
		||||
          _expect(url).not.toContain('folderToRename')
 | 
			
		||||
 | 
			
		||||
        await expect(projectMenuButton).toContainText('main.kcl')
 | 
			
		||||
        await expect(renamedFolder).toBeVisible()
 | 
			
		||||
        await expect(folderToRename).not.toBeAttached()
 | 
			
		||||
        expect(checkUnRenamedFolderFS()).toBeFalsy()
 | 
			
		||||
        expect(checkRenamedFolderFS()).toBeTruthy()
 | 
			
		||||
      })
 | 
			
		||||
          await _expect(projectMenuButton).toContainText('main.kcl')
 | 
			
		||||
          await _expect(renamedFolder).toBeVisible()
 | 
			
		||||
          await _expect(folderToRename).not.toBeAttached()
 | 
			
		||||
          _expect(checkUnRenamedFolderFS()).toBeFalsy()
 | 
			
		||||
          _expect(checkRenamedFolderFS()).toBeTruthy()
 | 
			
		||||
        }
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
      await electronApp.close()
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  test(
 | 
			
		||||
  _test(
 | 
			
		||||
    `A folder you are inside`,
 | 
			
		||||
    { tag: '@electron' },
 | 
			
		||||
    async ({ browser: _ }, testInfo) => {
 | 
			
		||||
@ -609,66 +688,69 @@ test.describe('Renaming in the file tree', () => {
 | 
			
		||||
        return fs.existsSync(folderPath)
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      await test.step('Open project and navigate into folder', async () => {
 | 
			
		||||
        await expect(projectLink).toBeVisible()
 | 
			
		||||
      await _test.step('Open project and navigate into folder', async () => {
 | 
			
		||||
        await _expect(projectLink).toBeVisible()
 | 
			
		||||
        await projectLink.click()
 | 
			
		||||
        await expect(projectMenuButton).toBeVisible()
 | 
			
		||||
        await expect(projectMenuButton).toContainText('main.kcl')
 | 
			
		||||
        await _expect(projectMenuButton).toBeVisible()
 | 
			
		||||
        await _expect(projectMenuButton).toContainText('main.kcl')
 | 
			
		||||
 | 
			
		||||
        const url = page.url()
 | 
			
		||||
        expect(url).toContain('main.kcl')
 | 
			
		||||
        expect(url).not.toContain('folderToRename')
 | 
			
		||||
        _expect(url).toContain('main.kcl')
 | 
			
		||||
        _expect(url).not.toContain('folderToRename')
 | 
			
		||||
 | 
			
		||||
        await u.openFilePanel()
 | 
			
		||||
        await expect(folderToRename).toBeVisible()
 | 
			
		||||
        await _expect(folderToRename).toBeVisible()
 | 
			
		||||
        await folderToRename.click()
 | 
			
		||||
        await expect(fileWithinFolder).toBeVisible()
 | 
			
		||||
        await _expect(fileWithinFolder).toBeVisible()
 | 
			
		||||
        await fileWithinFolder.click()
 | 
			
		||||
 | 
			
		||||
        await expect(projectMenuButton).toContainText('someFileWithin.kcl')
 | 
			
		||||
        await _expect(projectMenuButton).toContainText('someFileWithin.kcl')
 | 
			
		||||
        const newUrl = page.url()
 | 
			
		||||
        expect(newUrl).toContain('folderToRename')
 | 
			
		||||
        expect(newUrl).toContain('someFileWithin.kcl')
 | 
			
		||||
        expect(newUrl).not.toContain('main.kcl')
 | 
			
		||||
        expect(checkUnRenamedFolderFS()).toBeTruthy()
 | 
			
		||||
        expect(checkRenamedFolderFS()).toBeFalsy()
 | 
			
		||||
        _expect(newUrl).toContain('folderToRename')
 | 
			
		||||
        _expect(newUrl).toContain('someFileWithin.kcl')
 | 
			
		||||
        _expect(newUrl).not.toContain('main.kcl')
 | 
			
		||||
        _expect(checkUnRenamedFolderFS()).toBeTruthy()
 | 
			
		||||
        _expect(checkRenamedFolderFS()).toBeFalsy()
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await test.step('Rename the folder', async () => {
 | 
			
		||||
      await _test.step('Rename the folder', async () => {
 | 
			
		||||
        await page.waitForTimeout(60000)
 | 
			
		||||
        await folderToRename.click({ button: 'right' })
 | 
			
		||||
        await expect(renameMenuItem).toBeVisible()
 | 
			
		||||
        await _expect(renameMenuItem).toBeVisible()
 | 
			
		||||
        await renameMenuItem.click()
 | 
			
		||||
        await expect(renameInput).toBeVisible()
 | 
			
		||||
        await _expect(renameInput).toBeVisible()
 | 
			
		||||
        await renameInput.fill(newFolderName)
 | 
			
		||||
        await page.keyboard.press('Enter')
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await test.step('Verify the folder is renamed, and navigated to new path', async () => {
 | 
			
		||||
        const urlSnippet = encodeURIComponent(
 | 
			
		||||
          join(newFolderName, 'someFileWithin.kcl')
 | 
			
		||||
        )
 | 
			
		||||
        await page.waitForURL(new RegExp(urlSnippet))
 | 
			
		||||
        await expect(projectMenuButton).toContainText('someFileWithin.kcl')
 | 
			
		||||
        await expect(renamedFolder).toBeVisible()
 | 
			
		||||
        await expect(folderToRename).not.toBeAttached()
 | 
			
		||||
      await _test.step(
 | 
			
		||||
        'Verify the folder is renamed, and navigated to new path',
 | 
			
		||||
        async () => {
 | 
			
		||||
          const urlSnippet = encodeURIComponent(
 | 
			
		||||
            join(newFolderName, 'someFileWithin.kcl')
 | 
			
		||||
          )
 | 
			
		||||
          await page.waitForURL(new RegExp(urlSnippet))
 | 
			
		||||
          await _expect(projectMenuButton).toContainText('someFileWithin.kcl')
 | 
			
		||||
          await _expect(renamedFolder).toBeVisible()
 | 
			
		||||
          await _expect(folderToRename).not.toBeAttached()
 | 
			
		||||
 | 
			
		||||
        // URL is synchronous, so we check the other stuff first
 | 
			
		||||
        const url = page.url()
 | 
			
		||||
        expect(url).not.toContain('main.kcl')
 | 
			
		||||
        expect(url).toContain(newFolderName)
 | 
			
		||||
        expect(url).toContain('someFileWithin.kcl')
 | 
			
		||||
        expect(checkUnRenamedFolderFS()).toBeFalsy()
 | 
			
		||||
        expect(checkRenamedFolderFS()).toBeTruthy()
 | 
			
		||||
      })
 | 
			
		||||
          // URL is synchronous, so we check the other stuff first
 | 
			
		||||
          const url = page.url()
 | 
			
		||||
          _expect(url).not.toContain('main.kcl')
 | 
			
		||||
          _expect(url).toContain(newFolderName)
 | 
			
		||||
          _expect(url).toContain('someFileWithin.kcl')
 | 
			
		||||
          _expect(checkUnRenamedFolderFS()).toBeFalsy()
 | 
			
		||||
          _expect(checkRenamedFolderFS()).toBeTruthy()
 | 
			
		||||
        }
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
      await electronApp.close()
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
test.describe('Deleting items from the file pane', () => {
 | 
			
		||||
  test(
 | 
			
		||||
_test.describe('Deleting items from the file pane', () => {
 | 
			
		||||
  _test(
 | 
			
		||||
    `delete file when main.kcl exists, navigate to main.kcl`,
 | 
			
		||||
    { tag: '@electron' },
 | 
			
		||||
    async ({ browserName }, testInfo) => {
 | 
			
		||||
@ -700,45 +782,48 @@ test.describe('Deleting items from the file pane', () => {
 | 
			
		||||
      const deleteMenuItem = page.getByRole('button', { name: 'Delete' })
 | 
			
		||||
      const deleteConfirmation = page.getByTestId('delete-confirmation')
 | 
			
		||||
 | 
			
		||||
      await test.step('Open project and navigate to fileToDelete.kcl', async () => {
 | 
			
		||||
        await projectCard.click()
 | 
			
		||||
        await u.waitForPageLoad()
 | 
			
		||||
        await u.openFilePanel()
 | 
			
		||||
      await _test.step(
 | 
			
		||||
        'Open project and navigate to fileToDelete.kcl',
 | 
			
		||||
        async () => {
 | 
			
		||||
          await projectCard.click()
 | 
			
		||||
          await u.waitForPageLoad()
 | 
			
		||||
          await u.openFilePanel()
 | 
			
		||||
 | 
			
		||||
        await fileToDelete.click()
 | 
			
		||||
        await u.waitForPageLoad()
 | 
			
		||||
        await u.openKclCodePanel()
 | 
			
		||||
        await expect(u.codeLocator).toContainText('getOppositeEdge(thing)')
 | 
			
		||||
        await u.closeKclCodePanel()
 | 
			
		||||
      })
 | 
			
		||||
          await fileToDelete.click()
 | 
			
		||||
          await u.waitForPageLoad()
 | 
			
		||||
          await u.openKclCodePanel()
 | 
			
		||||
          await _expect(u.codeLocator).toContainText('getOppositeEdge(thing)')
 | 
			
		||||
          await u.closeKclCodePanel()
 | 
			
		||||
        }
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
      await test.step('Delete fileToDelete.kcl', async () => {
 | 
			
		||||
      await _test.step('Delete fileToDelete.kcl', async () => {
 | 
			
		||||
        await fileToDelete.click({ button: 'right' })
 | 
			
		||||
        await expect(deleteMenuItem).toBeVisible()
 | 
			
		||||
        await _expect(deleteMenuItem).toBeVisible()
 | 
			
		||||
        await deleteMenuItem.click()
 | 
			
		||||
        await expect(deleteConfirmation).toBeVisible()
 | 
			
		||||
        await _expect(deleteConfirmation).toBeVisible()
 | 
			
		||||
        await deleteConfirmation.click()
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await test.step('Check deletion and navigation', async () => {
 | 
			
		||||
      await _test.step('Check deletion and navigation', async () => {
 | 
			
		||||
        await u.waitForPageLoad()
 | 
			
		||||
        await expect(fileToDelete).not.toBeVisible()
 | 
			
		||||
        await _expect(fileToDelete).not.toBeVisible()
 | 
			
		||||
        await u.closeFilePanel()
 | 
			
		||||
        await u.openKclCodePanel()
 | 
			
		||||
        await expect(u.codeLocator).toContainText('circle(')
 | 
			
		||||
        await expect(projectMenuButton).toContainText('main.kcl')
 | 
			
		||||
        await _expect(u.codeLocator).toContainText('circle(')
 | 
			
		||||
        await _expect(projectMenuButton).toContainText('main.kcl')
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await electronApp.close()
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  test.fixme(
 | 
			
		||||
  _test.fixme(
 | 
			
		||||
    'TODO - delete file we have open when main.kcl does not exist',
 | 
			
		||||
    async () => {}
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  test(
 | 
			
		||||
  _test(
 | 
			
		||||
    `Delete folder we are not in, don't navigate`,
 | 
			
		||||
    { tag: '@electron' },
 | 
			
		||||
    async ({ browserName }, testInfo) => {
 | 
			
		||||
@ -772,32 +857,32 @@ test.describe('Deleting items from the file pane', () => {
 | 
			
		||||
      const deleteMenuItem = page.getByRole('button', { name: 'Delete' })
 | 
			
		||||
      const deleteConfirmation = page.getByTestId('delete-confirmation')
 | 
			
		||||
 | 
			
		||||
      await test.step('Open project and open project pane', async () => {
 | 
			
		||||
      await _test.step('Open project and open project pane', async () => {
 | 
			
		||||
        await projectCard.click()
 | 
			
		||||
        await u.waitForPageLoad()
 | 
			
		||||
        await expect(projectMenuButton).toContainText('main.kcl')
 | 
			
		||||
        await _expect(projectMenuButton).toContainText('main.kcl')
 | 
			
		||||
        await u.closeKclCodePanel()
 | 
			
		||||
        await u.openFilePanel()
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await test.step('Delete folderToDelete', async () => {
 | 
			
		||||
      await _test.step('Delete folderToDelete', async () => {
 | 
			
		||||
        await folderToDelete.click({ button: 'right' })
 | 
			
		||||
        await expect(deleteMenuItem).toBeVisible()
 | 
			
		||||
        await _expect(deleteMenuItem).toBeVisible()
 | 
			
		||||
        await deleteMenuItem.click()
 | 
			
		||||
        await expect(deleteConfirmation).toBeVisible()
 | 
			
		||||
        await _expect(deleteConfirmation).toBeVisible()
 | 
			
		||||
        await deleteConfirmation.click()
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await test.step('Check deletion and no navigation', async () => {
 | 
			
		||||
        await expect(folderToDelete).not.toBeAttached()
 | 
			
		||||
        await expect(projectMenuButton).toContainText('main.kcl')
 | 
			
		||||
      await _test.step('Check deletion and no navigation', async () => {
 | 
			
		||||
        await _expect(folderToDelete).not.toBeAttached()
 | 
			
		||||
        await _expect(projectMenuButton).toContainText('main.kcl')
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await electronApp.close()
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  test(
 | 
			
		||||
  _test(
 | 
			
		||||
    `Delete folder we are in, navigate to main.kcl`,
 | 
			
		||||
    { tag: '@electron' },
 | 
			
		||||
    async ({ browserName }, testInfo) => {
 | 
			
		||||
@ -834,36 +919,45 @@ test.describe('Deleting items from the file pane', () => {
 | 
			
		||||
      const deleteMenuItem = page.getByRole('button', { name: 'Delete' })
 | 
			
		||||
      const deleteConfirmation = page.getByTestId('delete-confirmation')
 | 
			
		||||
 | 
			
		||||
      await test.step('Open project and navigate into folderToDelete', async () => {
 | 
			
		||||
        await projectCard.click()
 | 
			
		||||
        await u.waitForPageLoad()
 | 
			
		||||
        await expect(projectMenuButton).toContainText('main.kcl')
 | 
			
		||||
        await u.closeKclCodePanel()
 | 
			
		||||
        await u.openFilePanel()
 | 
			
		||||
      await _test.step(
 | 
			
		||||
        'Open project and navigate into folderToDelete',
 | 
			
		||||
        async () => {
 | 
			
		||||
          await projectCard.click()
 | 
			
		||||
          await u.waitForPageLoad()
 | 
			
		||||
          await _expect(projectMenuButton).toContainText('main.kcl')
 | 
			
		||||
          await u.closeKclCodePanel()
 | 
			
		||||
          await u.openFilePanel()
 | 
			
		||||
 | 
			
		||||
        await folderToDelete.click()
 | 
			
		||||
        await expect(fileWithinFolder).toBeVisible()
 | 
			
		||||
        await fileWithinFolder.click()
 | 
			
		||||
        await expect(projectMenuButton).toContainText('someFileWithin.kcl')
 | 
			
		||||
      })
 | 
			
		||||
          await folderToDelete.click()
 | 
			
		||||
          await _expect(fileWithinFolder).toBeVisible()
 | 
			
		||||
          await fileWithinFolder.click()
 | 
			
		||||
          await _expect(projectMenuButton).toContainText('someFileWithin.kcl')
 | 
			
		||||
        }
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
      await test.step('Delete folderToDelete', async () => {
 | 
			
		||||
      await _test.step('Delete folderToDelete', async () => {
 | 
			
		||||
        await folderToDelete.click({ button: 'right' })
 | 
			
		||||
        await expect(deleteMenuItem).toBeVisible()
 | 
			
		||||
        await _expect(deleteMenuItem).toBeVisible()
 | 
			
		||||
        await deleteMenuItem.click()
 | 
			
		||||
        await expect(deleteConfirmation).toBeVisible()
 | 
			
		||||
        await _expect(deleteConfirmation).toBeVisible()
 | 
			
		||||
        await deleteConfirmation.click()
 | 
			
		||||
      })
 | 
			
		||||
 | 
			
		||||
      await test.step('Check deletion and navigation to main.kcl', async () => {
 | 
			
		||||
        await expect(folderToDelete).not.toBeAttached()
 | 
			
		||||
        await expect(fileWithinFolder).not.toBeAttached()
 | 
			
		||||
        await expect(projectMenuButton).toContainText('main.kcl')
 | 
			
		||||
      })
 | 
			
		||||
      await _test.step(
 | 
			
		||||
        'Check deletion and navigation to main.kcl',
 | 
			
		||||
        async () => {
 | 
			
		||||
          await _expect(folderToDelete).not.toBeAttached()
 | 
			
		||||
          await _expect(fileWithinFolder).not.toBeAttached()
 | 
			
		||||
          await _expect(projectMenuButton).toContainText('main.kcl')
 | 
			
		||||
        }
 | 
			
		||||
      )
 | 
			
		||||
 | 
			
		||||
      await electronApp.close()
 | 
			
		||||
    }
 | 
			
		||||
  )
 | 
			
		||||
 | 
			
		||||
  test.fixme('TODO - delete folder we are in, with no main.kcl', async () => {})
 | 
			
		||||
  _test.fixme(
 | 
			
		||||
    'TODO - delete folder we are in, with no main.kcl',
 | 
			
		||||
    async () => {}
 | 
			
		||||
  )
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
@ -1,70 +0,0 @@
 | 
			
		||||
import type { Page } from '@playwright/test'
 | 
			
		||||
import { test as base } from '@playwright/test'
 | 
			
		||||
import { getUtils, setup, tearDown } from './test-utils'
 | 
			
		||||
import fsp from 'fs/promises'
 | 
			
		||||
import { join } from 'path'
 | 
			
		||||
import { CmdBarFixture } from './cmdBarFixture'
 | 
			
		||||
import { EditorFixture } from './editorFixture'
 | 
			
		||||
import { ToolbarFixture } from './toolbarFixture'
 | 
			
		||||
import { SceneFixture } from './sceneFixture'
 | 
			
		||||
 | 
			
		||||
export class AuthenticatedApp {
 | 
			
		||||
  public readonly page: Page
 | 
			
		||||
 | 
			
		||||
  constructor(page: Page) {
 | 
			
		||||
    this.page = page
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async initialise(code = '') {
 | 
			
		||||
    const u = await getUtils(this.page)
 | 
			
		||||
 | 
			
		||||
    await this.page.addInitScript(async (code) => {
 | 
			
		||||
      localStorage.setItem('persistCode', code)
 | 
			
		||||
      ;(window as any).playwrightSkipFilePicker = true
 | 
			
		||||
    }, code)
 | 
			
		||||
 | 
			
		||||
    await this.page.setViewportSize({ width: 1000, height: 500 })
 | 
			
		||||
 | 
			
		||||
    await u.waitForAuthSkipAppStart()
 | 
			
		||||
  }
 | 
			
		||||
  getInputFile = (fileName: string) => {
 | 
			
		||||
    return fsp.readFile(
 | 
			
		||||
      join('src', 'wasm-lib', 'tests', 'executor', 'inputs', fileName),
 | 
			
		||||
      'utf-8'
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const test = base.extend<{
 | 
			
		||||
  app: AuthenticatedApp
 | 
			
		||||
  cmdBar: CmdBarFixture
 | 
			
		||||
  editor: EditorFixture
 | 
			
		||||
  toolbar: ToolbarFixture
 | 
			
		||||
  scene: SceneFixture
 | 
			
		||||
}>({
 | 
			
		||||
  app: async ({ page }, use) => {
 | 
			
		||||
    await use(new AuthenticatedApp(page))
 | 
			
		||||
  },
 | 
			
		||||
  cmdBar: async ({ page }, use) => {
 | 
			
		||||
    await use(new CmdBarFixture(page))
 | 
			
		||||
  },
 | 
			
		||||
  editor: async ({ page }, use) => {
 | 
			
		||||
    await use(new EditorFixture(page))
 | 
			
		||||
  },
 | 
			
		||||
  toolbar: async ({ page }, use) => {
 | 
			
		||||
    await use(new ToolbarFixture(page))
 | 
			
		||||
  },
 | 
			
		||||
  scene: async ({ page }, use) => {
 | 
			
		||||
    await use(new SceneFixture(page))
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
test.beforeEach(async ({ context, page }, testInfo) => {
 | 
			
		||||
  await setup(context, page, testInfo)
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
test.afterEach(async ({ page }, testInfo) => {
 | 
			
		||||
  await tearDown(page, testInfo)
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export { expect } from '@playwright/test'
 | 
			
		||||
@ -25,11 +25,14 @@ type CmdBarSerialised =
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
export class CmdBarFixture {
 | 
			
		||||
  public readonly page: Page
 | 
			
		||||
  public page: Page
 | 
			
		||||
 | 
			
		||||
  constructor(page: Page) {
 | 
			
		||||
    this.page = page
 | 
			
		||||
  }
 | 
			
		||||
  reConstruct = (page: Page) => {
 | 
			
		||||
    this.page = page
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _serialiseCmdBar = async (): Promise<CmdBarSerialised> => {
 | 
			
		||||
    const reviewForm = await this.page.locator('#review-form')
 | 
			
		||||
@ -1,5 +1,6 @@
 | 
			
		||||
import type { Page, Locator } from '@playwright/test'
 | 
			
		||||
import { expect } from '@playwright/test'
 | 
			
		||||
import { sansWhitespace } from '../test-utils'
 | 
			
		||||
 | 
			
		||||
interface EditorState {
 | 
			
		||||
  activeLines: Array<string>
 | 
			
		||||
@ -7,19 +8,20 @@ interface EditorState {
 | 
			
		||||
  diagnostics: Array<string>
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function removeWhitespace(str: string) {
 | 
			
		||||
  return str.replace(/\s+/g, '').trim()
 | 
			
		||||
}
 | 
			
		||||
export class EditorFixture {
 | 
			
		||||
  public readonly page: Page
 | 
			
		||||
  public page: Page
 | 
			
		||||
 | 
			
		||||
  private readonly diagnosticsTooltip: Locator
 | 
			
		||||
  private readonly diagnosticsGutterIcon: Locator
 | 
			
		||||
  private readonly codeContent: Locator
 | 
			
		||||
  private readonly activeLine: Locator
 | 
			
		||||
  private diagnosticsTooltip!: Locator
 | 
			
		||||
  private diagnosticsGutterIcon!: Locator
 | 
			
		||||
  private codeContent!: Locator
 | 
			
		||||
  private activeLine!: Locator
 | 
			
		||||
 | 
			
		||||
  constructor(page: Page) {
 | 
			
		||||
    this.page = page
 | 
			
		||||
    this.reConstruct(page)
 | 
			
		||||
  }
 | 
			
		||||
  reConstruct = (page: Page) => {
 | 
			
		||||
    this.page = page
 | 
			
		||||
 | 
			
		||||
    this.codeContent = page.locator('.cm-content')
 | 
			
		||||
    this.diagnosticsTooltip = page.locator('.cm-tooltip-lint')
 | 
			
		||||
@ -94,16 +96,16 @@ export class EditorFixture {
 | 
			
		||||
          this._serialiseDiagnostics(),
 | 
			
		||||
        ])
 | 
			
		||||
        const state: EditorState = {
 | 
			
		||||
          activeLines: activeLines.map(removeWhitespace).filter(Boolean),
 | 
			
		||||
          highlightedCode: removeWhitespace(highlightedCode),
 | 
			
		||||
          activeLines: activeLines.map(sansWhitespace).filter(Boolean),
 | 
			
		||||
          highlightedCode: sansWhitespace(highlightedCode),
 | 
			
		||||
          diagnostics,
 | 
			
		||||
        }
 | 
			
		||||
        return state
 | 
			
		||||
      })
 | 
			
		||||
      .toEqual({
 | 
			
		||||
        activeLines: expectedState.activeLines.map(removeWhitespace),
 | 
			
		||||
        highlightedCode: removeWhitespace(expectedState.highlightedCode),
 | 
			
		||||
        diagnostics: expectedState.diagnostics.map(removeWhitespace),
 | 
			
		||||
        activeLines: expectedState.activeLines.map(sansWhitespace),
 | 
			
		||||
        highlightedCode: sansWhitespace(expectedState.highlightedCode),
 | 
			
		||||
        diagnostics: expectedState.diagnostics.map(sansWhitespace),
 | 
			
		||||
      })
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										140
									
								
								e2e/playwright/fixtures/fixtureSetup.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,140 @@
 | 
			
		||||
import type {
 | 
			
		||||
  BrowserContext,
 | 
			
		||||
  ElectronApplication,
 | 
			
		||||
  Page,
 | 
			
		||||
  TestInfo,
 | 
			
		||||
} from '@playwright/test'
 | 
			
		||||
import { test as base } from '@playwright/test'
 | 
			
		||||
import { getUtils, setup, setupElectron, tearDown } from '../test-utils'
 | 
			
		||||
import fsp from 'fs/promises'
 | 
			
		||||
import { join } from 'path'
 | 
			
		||||
import { CmdBarFixture } from './cmdBarFixture'
 | 
			
		||||
import { EditorFixture } from './editorFixture'
 | 
			
		||||
import { ToolbarFixture } from './toolbarFixture'
 | 
			
		||||
import { SceneFixture } from './sceneFixture'
 | 
			
		||||
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
 | 
			
		||||
import { HomePageFixture } from './homePageFixture'
 | 
			
		||||
import { unsafeTypedKeys } from 'lib/utils'
 | 
			
		||||
 | 
			
		||||
export class AuthenticatedApp {
 | 
			
		||||
  public readonly page: Page
 | 
			
		||||
  public readonly context: BrowserContext
 | 
			
		||||
  public readonly testInfo: TestInfo
 | 
			
		||||
 | 
			
		||||
  constructor(context: BrowserContext, page: Page, testInfo: TestInfo) {
 | 
			
		||||
    this.page = page
 | 
			
		||||
    this.context = context
 | 
			
		||||
    this.testInfo = testInfo
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async initialise(code = '') {
 | 
			
		||||
    await setup(this.context, this.page, this.testInfo)
 | 
			
		||||
    const u = await getUtils(this.page)
 | 
			
		||||
 | 
			
		||||
    await this.page.addInitScript(async (code) => {
 | 
			
		||||
      localStorage.setItem('persistCode', code)
 | 
			
		||||
      ;(window as any).playwrightSkipFilePicker = true
 | 
			
		||||
    }, code)
 | 
			
		||||
 | 
			
		||||
    await this.page.setViewportSize({ width: 1000, height: 500 })
 | 
			
		||||
 | 
			
		||||
    await u.waitForAuthSkipAppStart()
 | 
			
		||||
  }
 | 
			
		||||
  getInputFile = (fileName: string) => {
 | 
			
		||||
    return fsp.readFile(
 | 
			
		||||
      join('src', 'wasm-lib', 'tests', 'executor', 'inputs', fileName),
 | 
			
		||||
      'utf-8'
 | 
			
		||||
    )
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface Fixtures {
 | 
			
		||||
  app: AuthenticatedApp
 | 
			
		||||
  tronApp: AuthenticatedTronApp
 | 
			
		||||
  cmdBar: CmdBarFixture
 | 
			
		||||
  editor: EditorFixture
 | 
			
		||||
  toolbar: ToolbarFixture
 | 
			
		||||
  scene: SceneFixture
 | 
			
		||||
  homePage: HomePageFixture
 | 
			
		||||
}
 | 
			
		||||
export class AuthenticatedTronApp {
 | 
			
		||||
  public readonly _page: Page
 | 
			
		||||
  public page: Page
 | 
			
		||||
  public readonly context: BrowserContext
 | 
			
		||||
  public readonly testInfo: TestInfo
 | 
			
		||||
  public electronApp?: ElectronApplication
 | 
			
		||||
 | 
			
		||||
  constructor(context: BrowserContext, page: Page, testInfo: TestInfo) {
 | 
			
		||||
    this._page = page
 | 
			
		||||
    this.page = page
 | 
			
		||||
    this.context = context
 | 
			
		||||
    this.testInfo = testInfo
 | 
			
		||||
  }
 | 
			
		||||
  async initialise(
 | 
			
		||||
    arg: {
 | 
			
		||||
      fixtures: Partial<Fixtures>
 | 
			
		||||
      folderSetupFn?: (projectDirName: string) => Promise<void>
 | 
			
		||||
      cleanProjectDir?: boolean
 | 
			
		||||
      appSettings?: Partial<SaveSettingsPayload>
 | 
			
		||||
    } = { fixtures: {} }
 | 
			
		||||
  ) {
 | 
			
		||||
    const { electronApp, page } = await setupElectron({
 | 
			
		||||
      testInfo: this.testInfo,
 | 
			
		||||
      folderSetupFn: arg.folderSetupFn,
 | 
			
		||||
      cleanProjectDir: arg.cleanProjectDir,
 | 
			
		||||
      appSettings: arg.appSettings,
 | 
			
		||||
    })
 | 
			
		||||
    this.page = page
 | 
			
		||||
    this.electronApp = electronApp
 | 
			
		||||
    await page.setViewportSize({ width: 1200, height: 500 })
 | 
			
		||||
 | 
			
		||||
    for (const key of unsafeTypedKeys(arg.fixtures)) {
 | 
			
		||||
      const fixture = arg.fixtures[key]
 | 
			
		||||
      if (
 | 
			
		||||
        !fixture ||
 | 
			
		||||
        fixture instanceof AuthenticatedApp ||
 | 
			
		||||
        fixture instanceof AuthenticatedTronApp
 | 
			
		||||
      )
 | 
			
		||||
        continue
 | 
			
		||||
      fixture.reConstruct(page)
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  close = async () => {
 | 
			
		||||
    await this.electronApp?.close?.()
 | 
			
		||||
  }
 | 
			
		||||
  debugPause = () =>
 | 
			
		||||
    new Promise(() => {
 | 
			
		||||
      console.log('UN-RESOLVING PROMISE')
 | 
			
		||||
    })
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const test = base.extend<Fixtures>({
 | 
			
		||||
  app: async ({ page, context }, use, testInfo) => {
 | 
			
		||||
    await use(new AuthenticatedApp(context, page, testInfo))
 | 
			
		||||
  },
 | 
			
		||||
  tronApp: async ({ page, context }, use, testInfo) => {
 | 
			
		||||
    await use(new AuthenticatedTronApp(context, page, testInfo))
 | 
			
		||||
  },
 | 
			
		||||
  cmdBar: async ({ page }, use) => {
 | 
			
		||||
    await use(new CmdBarFixture(page))
 | 
			
		||||
  },
 | 
			
		||||
  editor: async ({ page }, use) => {
 | 
			
		||||
    await use(new EditorFixture(page))
 | 
			
		||||
  },
 | 
			
		||||
  toolbar: async ({ page }, use) => {
 | 
			
		||||
    await use(new ToolbarFixture(page))
 | 
			
		||||
  },
 | 
			
		||||
  scene: async ({ page }, use) => {
 | 
			
		||||
    await use(new SceneFixture(page))
 | 
			
		||||
  },
 | 
			
		||||
  homePage: async ({ page }, use) => {
 | 
			
		||||
    await use(new HomePageFixture(page))
 | 
			
		||||
  },
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
test.afterEach(async ({ page }, testInfo) => {
 | 
			
		||||
  await tearDown(page, testInfo)
 | 
			
		||||
})
 | 
			
		||||
 | 
			
		||||
export { expect } from '@playwright/test'
 | 
			
		||||
							
								
								
									
										103
									
								
								e2e/playwright/fixtures/homePageFixture.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,103 @@
 | 
			
		||||
import type { Page, Locator } from '@playwright/test'
 | 
			
		||||
import { expect } from '@playwright/test'
 | 
			
		||||
 | 
			
		||||
interface ProjectCardState {
 | 
			
		||||
  title: string
 | 
			
		||||
  fileCount: number
 | 
			
		||||
  folderCount: number
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
interface HomePageState {
 | 
			
		||||
  projectCards: ProjectCardState[]
 | 
			
		||||
  sortBy: 'last-modified-desc' | 'last-modified-asc' | 'name-asc' | 'name-desc'
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class HomePageFixture {
 | 
			
		||||
  public page: Page
 | 
			
		||||
 | 
			
		||||
  projectCard!: Locator
 | 
			
		||||
  projectCardTitle!: Locator
 | 
			
		||||
  projectCardFile!: Locator
 | 
			
		||||
  projectCardFolder!: Locator
 | 
			
		||||
  sortByDateBtn!: Locator
 | 
			
		||||
  sortByNameBtn!: Locator
 | 
			
		||||
 | 
			
		||||
  constructor(page: Page) {
 | 
			
		||||
    this.page = page
 | 
			
		||||
    this.reConstruct(page)
 | 
			
		||||
  }
 | 
			
		||||
  reConstruct = (page: Page) => {
 | 
			
		||||
    this.page = page
 | 
			
		||||
 | 
			
		||||
    this.projectCard = this.page.getByTestId('project-link')
 | 
			
		||||
    this.projectCardTitle = this.page.getByTestId('project-title')
 | 
			
		||||
    this.projectCardFile = this.page.getByTestId('project-file-count')
 | 
			
		||||
    this.projectCardFolder = this.page.getByTestId('project-folder-count')
 | 
			
		||||
 | 
			
		||||
    this.sortByDateBtn = this.page.getByTestId('home-sort-by-modified')
 | 
			
		||||
    this.sortByNameBtn = this.page.getByTestId('home-sort-by-name')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _serialiseSortBy = async (): Promise<
 | 
			
		||||
    HomePageState['sortBy'] | null
 | 
			
		||||
  > => {
 | 
			
		||||
    const [dateBtnDesc, dateBtnAsc, nameBtnDesc, nameBtnAsc] =
 | 
			
		||||
      await Promise.all([
 | 
			
		||||
        this.sortByDateBtn.getByLabel('arrow down').isVisible(),
 | 
			
		||||
        this.sortByDateBtn.getByLabel('arrow up').isVisible(),
 | 
			
		||||
        this.sortByNameBtn.getByLabel('arrow down').isVisible(),
 | 
			
		||||
        this.sortByNameBtn.getByLabel('arrow up').isVisible(),
 | 
			
		||||
      ])
 | 
			
		||||
    if (dateBtnDesc) return 'last-modified-desc'
 | 
			
		||||
    if (dateBtnAsc) return 'last-modified-asc'
 | 
			
		||||
    if (nameBtnDesc) return 'name-desc'
 | 
			
		||||
    if (nameBtnAsc) return 'name-asc'
 | 
			
		||||
    return null
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _serialiseProjectCards = async (): Promise<
 | 
			
		||||
    Array<ProjectCardState>
 | 
			
		||||
  > => {
 | 
			
		||||
    const projectCards = await this.projectCard.all()
 | 
			
		||||
    const projectCardStates: Array<ProjectCardState> = []
 | 
			
		||||
    for (const projectCard of projectCards) {
 | 
			
		||||
      const [title, fileCount, folderCount] = await Promise.all([
 | 
			
		||||
        (await projectCard.locator(this.projectCardTitle).textContent()) || '',
 | 
			
		||||
        Number(await projectCard.locator(this.projectCardFile).textContent()),
 | 
			
		||||
        Number(await projectCard.locator(this.projectCardFolder).textContent()),
 | 
			
		||||
      ])
 | 
			
		||||
      projectCardStates.push({
 | 
			
		||||
        title: title,
 | 
			
		||||
        fileCount,
 | 
			
		||||
        folderCount,
 | 
			
		||||
      })
 | 
			
		||||
    }
 | 
			
		||||
    return projectCardStates
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Date is excluded from expectState, since it changes
 | 
			
		||||
   * Maybe there a good sanity check we can do each time?
 | 
			
		||||
   */
 | 
			
		||||
  expectState = async (expectedState: HomePageState) => {
 | 
			
		||||
    await expect
 | 
			
		||||
      .poll(async () => {
 | 
			
		||||
        const [projectCards, sortBy] = await Promise.all([
 | 
			
		||||
          this._serialiseProjectCards(),
 | 
			
		||||
          this._serialiseSortBy(),
 | 
			
		||||
        ])
 | 
			
		||||
        return {
 | 
			
		||||
          projectCards,
 | 
			
		||||
          sortBy,
 | 
			
		||||
        }
 | 
			
		||||
      })
 | 
			
		||||
      .toEqual(expectedState)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  openProject = async (projectTitle: string) => {
 | 
			
		||||
    const projectCard = this.projectCard.locator(
 | 
			
		||||
      this.page.getByText(projectTitle)
 | 
			
		||||
    )
 | 
			
		||||
    await projectCard.click()
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@ -6,18 +6,24 @@ import {
 | 
			
		||||
  doAndWaitForImageDiff,
 | 
			
		||||
  openAndClearDebugPanel,
 | 
			
		||||
  sendCustomCmd,
 | 
			
		||||
} from './test-utils'
 | 
			
		||||
} from '../test-utils'
 | 
			
		||||
 | 
			
		||||
type mouseParams = {
 | 
			
		||||
  pixelDiff: number
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export class SceneFixture {
 | 
			
		||||
  public readonly page: Page
 | 
			
		||||
  private readonly exeIndicator: Locator
 | 
			
		||||
  public page: Page
 | 
			
		||||
 | 
			
		||||
  private exeIndicator!: Locator
 | 
			
		||||
 | 
			
		||||
  constructor(page: Page) {
 | 
			
		||||
    this.page = page
 | 
			
		||||
    this.reConstruct(page)
 | 
			
		||||
  }
 | 
			
		||||
  reConstruct = (page: Page) => {
 | 
			
		||||
    this.page = page
 | 
			
		||||
 | 
			
		||||
    this.exeIndicator = page.getByTestId('model-state-indicator-execution-done')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@ -25,33 +31,38 @@ export class SceneFixture {
 | 
			
		||||
    x: number,
 | 
			
		||||
    y: number,
 | 
			
		||||
    { steps }: { steps: number } = { steps: 5000 }
 | 
			
		||||
  ) => [
 | 
			
		||||
    (params?: mouseParams) => {
 | 
			
		||||
      if (params?.pixelDiff) {
 | 
			
		||||
        return doAndWaitForImageDiff(
 | 
			
		||||
          this.page,
 | 
			
		||||
          () => this.page.mouse.click(x, y),
 | 
			
		||||
          params.pixelDiff
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
      return this.page.mouse.click(x, y)
 | 
			
		||||
    },
 | 
			
		||||
    (params?: mouseParams) => {
 | 
			
		||||
      if (params?.pixelDiff) {
 | 
			
		||||
        return doAndWaitForImageDiff(
 | 
			
		||||
          this.page,
 | 
			
		||||
          () => this.page.mouse.move(x, y, { steps }),
 | 
			
		||||
          params.pixelDiff
 | 
			
		||||
        )
 | 
			
		||||
      }
 | 
			
		||||
      return this.page.mouse.move(x, y, { steps })
 | 
			
		||||
    },
 | 
			
		||||
  ]
 | 
			
		||||
  ) =>
 | 
			
		||||
    [
 | 
			
		||||
      (clickParams?: mouseParams) => {
 | 
			
		||||
        if (clickParams?.pixelDiff) {
 | 
			
		||||
          return doAndWaitForImageDiff(
 | 
			
		||||
            this.page,
 | 
			
		||||
            () => this.page.mouse.click(x, y),
 | 
			
		||||
            clickParams.pixelDiff
 | 
			
		||||
          )
 | 
			
		||||
        }
 | 
			
		||||
        return this.page.mouse.click(x, y)
 | 
			
		||||
      },
 | 
			
		||||
      (moveParams?: mouseParams) => {
 | 
			
		||||
        if (moveParams?.pixelDiff) {
 | 
			
		||||
          return doAndWaitForImageDiff(
 | 
			
		||||
            this.page,
 | 
			
		||||
            () => this.page.mouse.move(x, y, { steps }),
 | 
			
		||||
            moveParams.pixelDiff
 | 
			
		||||
          )
 | 
			
		||||
        }
 | 
			
		||||
        return this.page.mouse.move(x, y, { steps })
 | 
			
		||||
      },
 | 
			
		||||
    ] as const
 | 
			
		||||
 | 
			
		||||
  /** Likely no where, there's a chance it will click something in the scene, depending what you have in the scene.
 | 
			
		||||
   *
 | 
			
		||||
   * Expects the viewPort to be 1000x500 */
 | 
			
		||||
  clickNoWhere = () => this.page.mouse.click(998, 60)
 | 
			
		||||
  /** Likely no where, there's a chance it will click something in the scene, depending what you have in the scene.
 | 
			
		||||
   *
 | 
			
		||||
   * Expects the viewPort to be 1000x500 */
 | 
			
		||||
  moveNoWhere = (steps?: number) => this.page.mouse.move(998, 60, { steps })
 | 
			
		||||
 | 
			
		||||
  moveCameraTo = async (
 | 
			
		||||
    pos: { x: number; y: number; z: number },
 | 
			
		||||
							
								
								
									
										79
									
								
								e2e/playwright/fixtures/toolbarFixture.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,79 @@
 | 
			
		||||
import type { Page, Locator } from '@playwright/test'
 | 
			
		||||
import { expect } from './fixtureSetup'
 | 
			
		||||
import { doAndWaitForImageDiff } from '../test-utils'
 | 
			
		||||
 | 
			
		||||
export class ToolbarFixture {
 | 
			
		||||
  public page: Page
 | 
			
		||||
 | 
			
		||||
  extrudeButton!: Locator
 | 
			
		||||
  startSketchBtn!: Locator
 | 
			
		||||
  rectangleBtn!: Locator
 | 
			
		||||
  exitSketchBtn!: Locator
 | 
			
		||||
  editSketchBtn!: Locator
 | 
			
		||||
  fileTreeBtn!: Locator
 | 
			
		||||
  createFileBtn!: Locator
 | 
			
		||||
  fileCreateToast!: Locator
 | 
			
		||||
  filePane!: Locator
 | 
			
		||||
  exeIndicator!: Locator
 | 
			
		||||
 | 
			
		||||
  constructor(page: Page) {
 | 
			
		||||
    this.page = page
 | 
			
		||||
    this.reConstruct(page)
 | 
			
		||||
  }
 | 
			
		||||
  reConstruct = (page: Page) => {
 | 
			
		||||
    this.page = page
 | 
			
		||||
    this.extrudeButton = page.getByTestId('extrude')
 | 
			
		||||
    this.startSketchBtn = page.getByTestId('sketch')
 | 
			
		||||
    this.rectangleBtn = page.getByTestId('corner-rectangle')
 | 
			
		||||
    this.exitSketchBtn = page.getByTestId('sketch-exit')
 | 
			
		||||
    this.editSketchBtn = page.getByText('Edit Sketch')
 | 
			
		||||
    this.fileTreeBtn = page.locator('[id="files-button-holder"]')
 | 
			
		||||
    this.createFileBtn = page.getByTestId('create-file-button')
 | 
			
		||||
 | 
			
		||||
    this.filePane = page.locator('#files-pane')
 | 
			
		||||
    this.fileCreateToast = page.getByText('Successfully created')
 | 
			
		||||
    this.exeIndicator = page.getByTestId('model-state-indicator-execution-done')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  startSketchPlaneSelection = async () =>
 | 
			
		||||
    doAndWaitForImageDiff(this.page, () => this.startSketchBtn.click(), 500)
 | 
			
		||||
 | 
			
		||||
  editSketch = async () => {
 | 
			
		||||
    await this.editSketchBtn.first().click()
 | 
			
		||||
    // One of the rare times we want to allow a arbitrary wait
 | 
			
		||||
    // this is for the engine animation, as it takes 500ms to complete
 | 
			
		||||
    await this.page.waitForTimeout(600)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _serialiseFileTree = async () => {
 | 
			
		||||
    return this.page
 | 
			
		||||
      .locator('#files-pane')
 | 
			
		||||
      .getByTestId('file-tree-item')
 | 
			
		||||
      .allInnerTexts()
 | 
			
		||||
  }
 | 
			
		||||
  /**
 | 
			
		||||
   * TODO folders, in expect state
 | 
			
		||||
   */
 | 
			
		||||
  expectFileTreeState = async (expected: string[]) => {
 | 
			
		||||
    await expect.poll(this._serialiseFileTree).toEqual(expected)
 | 
			
		||||
  }
 | 
			
		||||
  createFile = async ({ wait }: { wait: boolean } = { wait: false }) => {
 | 
			
		||||
    await this.createFileBtn.click()
 | 
			
		||||
    await expect(this.fileCreateToast).toBeVisible()
 | 
			
		||||
    if (wait) {
 | 
			
		||||
      await this.fileCreateToast.waitFor({ state: 'detached' })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  /**
 | 
			
		||||
   * Opens file by it's name and waits for execution to finish
 | 
			
		||||
   */
 | 
			
		||||
  openFile = async (
 | 
			
		||||
    fileName: string,
 | 
			
		||||
    { wait }: { wait?: boolean } = { wait: true }
 | 
			
		||||
  ) => {
 | 
			
		||||
    await this.filePane.getByText(fileName).click()
 | 
			
		||||
    if (wait) {
 | 
			
		||||
      await expect(this.exeIndicator).toBeVisible({ timeout: 15_000 })
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										7
									
								
								e2e/playwright/playwright-deprecated.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@ -0,0 +1,7 @@
 | 
			
		||||
import { test, expect } from '@playwright/test'
 | 
			
		||||
 | 
			
		||||
/** @deprecated, import from ./fixtureSetup.ts instead */
 | 
			
		||||
export const _test = test
 | 
			
		||||
 | 
			
		||||
/** @deprecated, import from ./fixtureSetup.ts instead */
 | 
			
		||||
export const _expect = expect
 | 
			
		||||
@ -1,7 +1,7 @@
 | 
			
		||||
import { test, expect, AuthenticatedApp } from './fixtureSetup'
 | 
			
		||||
import { EditorFixture } from './editorFixture'
 | 
			
		||||
import { SceneFixture } from './sceneFixture'
 | 
			
		||||
import { ToolbarFixture } from './toolbarFixture'
 | 
			
		||||
import { test, expect, AuthenticatedApp } from './fixtures/fixtureSetup'
 | 
			
		||||
import { EditorFixture } from './fixtures/editorFixture'
 | 
			
		||||
import { SceneFixture } from './fixtures/sceneFixture'
 | 
			
		||||
import { ToolbarFixture } from './fixtures/toolbarFixture'
 | 
			
		||||
 | 
			
		||||
// test file is for testing point an click code gen functionality that's not sketch mode related
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -474,6 +474,7 @@ test(
 | 
			
		||||
 | 
			
		||||
    await expect(page).toHaveScreenshot({
 | 
			
		||||
      maxDiffPixels: 100,
 | 
			
		||||
      mask: [page.getByTestId('model-state-indicator')],
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
@ -531,6 +532,7 @@ test(
 | 
			
		||||
    // Ensure the draft rectangle looks the same as it usually does
 | 
			
		||||
    await expect(page).toHaveScreenshot({
 | 
			
		||||
      maxDiffPixels: 100,
 | 
			
		||||
      mask: [page.getByTestId('model-state-indicator')],
 | 
			
		||||
    })
 | 
			
		||||
  }
 | 
			
		||||
)
 | 
			
		||||
@ -585,6 +587,7 @@ test(
 | 
			
		||||
    // Ensure the draft rectangle looks the same as it usually does
 | 
			
		||||
    await expect(page).toHaveScreenshot({
 | 
			
		||||
      maxDiffPixels: 100,
 | 
			
		||||
      mask: [page.getByTestId('model-state-indicator')],
 | 
			
		||||
    })
 | 
			
		||||
    await expect(page.locator('.cm-content')).toHaveText(
 | 
			
		||||
      `const sketch001 = startSketchOn('XZ')
 | 
			
		||||
 | 
			
		||||
| 
		 Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB  | 
| 
		 Before Width: | Height: | Size: 42 KiB After Width: | Height: | Size: 42 KiB  | 
| 
		 Before Width: | Height: | Size: 44 KiB After Width: | Height: | Size: 44 KiB  | 
| 
		 Before Width: | Height: | Size: 41 KiB After Width: | Height: | Size: 41 KiB  | 
| 
		 Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 49 KiB  | 
| 
		 Before Width: | Height: | Size: 47 KiB After Width: | Height: | Size: 47 KiB  | 
@ -1066,3 +1066,7 @@ export async function openAndClearDebugPanel(page: Page) {
 | 
			
		||||
  await openDebugPanel(page)
 | 
			
		||||
  return clearCommandLogs(page)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function sansWhitespace(str: string) {
 | 
			
		||||
  return str.replace(/\s+/g, '').trim()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -1,21 +0,0 @@
 | 
			
		||||
import type { Page, Locator } from '@playwright/test'
 | 
			
		||||
import { doAndWaitForImageDiff } from './test-utils'
 | 
			
		||||
 | 
			
		||||
export class ToolbarFixture {
 | 
			
		||||
  public readonly page: Page
 | 
			
		||||
  readonly extrudeButton: Locator
 | 
			
		||||
  readonly startSketchBtn: Locator
 | 
			
		||||
  readonly rectangleBtn: Locator
 | 
			
		||||
  readonly exitSketchBtn: Locator
 | 
			
		||||
 | 
			
		||||
  constructor(page: Page) {
 | 
			
		||||
    this.page = page
 | 
			
		||||
    this.extrudeButton = page.getByTestId('extrude')
 | 
			
		||||
    this.startSketchBtn = page.getByTestId('sketch')
 | 
			
		||||
    this.rectangleBtn = page.getByTestId('corner-rectangle')
 | 
			
		||||
    this.exitSketchBtn = page.getByTestId('sketch-exit')
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  startSketchPlaneSelection = async () =>
 | 
			
		||||
    doAndWaitForImageDiff(this.page, () => this.startSketchBtn.click(), 500)
 | 
			
		||||
}
 | 
			
		||||
@ -194,7 +194,7 @@ const FileTreeItem = ({
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div className="contents" ref={itemRef}>
 | 
			
		||||
    <div className="contents" data-testid="file-tree-item" ref={itemRef}>
 | 
			
		||||
      {fileOrDir.children === null ? (
 | 
			
		||||
        <li
 | 
			
		||||
          className={
 | 
			
		||||
@ -389,12 +389,14 @@ interface FileTreeProps {
 | 
			
		||||
 | 
			
		||||
export const FileTreeMenu = () => {
 | 
			
		||||
  const { send } = useFileContext()
 | 
			
		||||
  const { send: modelingSend } = useModelingContext()
 | 
			
		||||
 | 
			
		||||
  function createFile() {
 | 
			
		||||
    send({
 | 
			
		||||
      type: 'Create file',
 | 
			
		||||
      data: { name: '', makeDir: false, shouldSetToRename: true },
 | 
			
		||||
    })
 | 
			
		||||
    modelingSend({ type: 'Cancel' })
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  function createFolder() {
 | 
			
		||||
 | 
			
		||||
@ -104,20 +104,33 @@ function ProjectCard({
 | 
			
		||||
              ref={inputRef}
 | 
			
		||||
            />
 | 
			
		||||
          ) : (
 | 
			
		||||
            <h3 className="font-sans relative z-0 p-2">
 | 
			
		||||
            <h3
 | 
			
		||||
              className="font-sans relative z-0 p-2"
 | 
			
		||||
              data-testid="project-title"
 | 
			
		||||
            >
 | 
			
		||||
              {project.name?.replace(FILE_EXT, '')}
 | 
			
		||||
            </h3>
 | 
			
		||||
          )}
 | 
			
		||||
          <span className="px-2 text-chalkboard-60 text-xs">
 | 
			
		||||
            {numberOfFiles} file{numberOfFiles === 1 ? '' : 's'}{' '}
 | 
			
		||||
            {numberOfFolders > 0 &&
 | 
			
		||||
              `/ ${numberOfFolders} folder${numberOfFolders === 1 ? '' : 's'}`}
 | 
			
		||||
            <span data-testid="project-file-count">{numberOfFiles}</span> file
 | 
			
		||||
            {numberOfFiles === 1 ? '' : 's'}{' '}
 | 
			
		||||
            {numberOfFolders > 0 && (
 | 
			
		||||
              <>
 | 
			
		||||
                {'/ '}
 | 
			
		||||
                <span data-testid="project-folder-count">
 | 
			
		||||
                  {numberOfFolders}
 | 
			
		||||
                </span>{' '}
 | 
			
		||||
                folder{numberOfFolders === 1 ? '' : 's'}
 | 
			
		||||
              </>
 | 
			
		||||
            )}
 | 
			
		||||
          </span>
 | 
			
		||||
          <span className="px-2 text-chalkboard-60 text-xs">
 | 
			
		||||
            Edited{' '}
 | 
			
		||||
            {project.metadata && project.metadata.modified
 | 
			
		||||
              ? getDisplayedTime(parseInt(project.metadata.modified))
 | 
			
		||||
              : 'never'}
 | 
			
		||||
            <span data-testid="project-edit-date">
 | 
			
		||||
              {project.metadata && project.metadata.modified
 | 
			
		||||
                ? getDisplayedTime(parseInt(project.metadata.modified))
 | 
			
		||||
                : 'never'}
 | 
			
		||||
            </span>
 | 
			
		||||
          </span>
 | 
			
		||||
        </div>
 | 
			
		||||
      </Link>
 | 
			
		||||
 | 
			
		||||
@ -15,6 +15,30 @@ export function isArray(val: any): val is unknown[] {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * An alternative to `Object.keys()` that returns an array of keys with types.
 | 
			
		||||
 *
 | 
			
		||||
 * It's UNSAFE because because of TS's structural subtyping and how at runtime, you can
 | 
			
		||||
 * extend a JS object with whatever keys you want.
 | 
			
		||||
 *
 | 
			
		||||
 * Why we shouldn't be extending objects with arbitrary keys at run time, the structural subtyping
 | 
			
		||||
 * issue could be a confusing bug, for example, in the below snippet `myKeys` is typed as
 | 
			
		||||
 * `('x' | 'y')[]` but is really `('x' | 'y' | 'name')[]`
 | 
			
		||||
 * ```ts
 | 
			
		||||
 * interface Point { x: number; y: number }
 | 
			
		||||
 * interface NamedPoint { x: number; y: number; name: string }
 | 
			
		||||
 *
 | 
			
		||||
 * let point: Point = { x: 1, y: 2 }
 | 
			
		||||
 * let namedPoint: NamedPoint = { x: 1, y: 2, name: 'A' }
 | 
			
		||||
 *
 | 
			
		||||
 * // Structural subtyping allows this assignment
 | 
			
		||||
 * point = namedPoint  // This is allowed because NamedPoint has all properties of Point
 | 
			
		||||
 * const myKeys = unsafeTypedKeys(point) // typed as ('x' | 'y')[] but is really ('x' | 'y' | 'name')[]
 | 
			
		||||
 * ```
 | 
			
		||||
 */
 | 
			
		||||
export function unsafeTypedKeys<T extends object>(obj: T): Array<keyof T> {
 | 
			
		||||
  return Object.keys(obj) as Array<keyof T>
 | 
			
		||||
}
 | 
			
		||||
/*
 | 
			
		||||
 * Predicate that checks if a value is not null and not undefined.  This is
 | 
			
		||||
 * useful for functions like Array::filter() and Array::find() that have
 | 
			
		||||
 * overloads that accept a type guard.
 | 
			
		||||
 | 
			
		||||
@ -248,6 +248,7 @@ const Home = () => {
 | 
			
		||||
              <small>Sort by</small>
 | 
			
		||||
              <ActionButton
 | 
			
		||||
                Element="button"
 | 
			
		||||
                data-testid="home-sort-by-name"
 | 
			
		||||
                className={
 | 
			
		||||
                  'text-xs border-primary/10 ' +
 | 
			
		||||
                  (!sort.includes('name')
 | 
			
		||||
@ -269,6 +270,7 @@ const Home = () => {
 | 
			
		||||
              </ActionButton>
 | 
			
		||||
              <ActionButton
 | 
			
		||||
                Element="button"
 | 
			
		||||
                data-testid="home-sort-by-modified"
 | 
			
		||||
                className={
 | 
			
		||||
                  'text-xs border-primary/10 ' +
 | 
			
		||||
                  (!isSortByModified
 | 
			
		||||
 | 
			
		||||