Fix path splitting issues on windows (#3565)
* Fix path splitting issues on windows * Fix path splitting issue on routeLoaders * Enable some e2e tests * Swap enabled e2e tests * Working bare-min project parse * Make tsc happy * Clean up & enable more tests * Fix paths in browser * Fix tests for windows fmt * Clean up wasm side * Make build:wasm windows compatible * More paths cleanup & some tests * Remove sleep * Use new config sturcture in parseroute * Clean up debugger * Fix: on settings close go back to the same file (#3549) * Fix: on settings close go back to the same file * A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu-latest) * shit aint working yo * Get that page a-loading * A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest) --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Frank Noirot <frank@kittycad.io> Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com> * Fmt * Comment out currently failing win32 tests * Ignore tsc for electron monkey-patch * Force line-endings to only * Fix tsc * Enable more tests * A snapshot a day keeps the bugs away! 📷🐛 (OS: windows-latest) * Avoid modifying global for tests --------- Co-authored-by: 49fl <ircsurfer33@gmail.com> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Frank Noirot <frank@kittycad.io> Co-authored-by: Jess Frazelle <jessfraz@users.noreply.github.com>
This commit is contained in:
@ -45,17 +45,20 @@ test(
|
||||
'click help/keybindings from project page',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
await fsp.mkdir(`${dir}/bracket`, { recursive: true })
|
||||
await fsp.mkdir(join(dir, 'bracket'), { recursive: true })
|
||||
await fsp.copyFile(
|
||||
'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl',
|
||||
`${dir}/bracket/main.kcl`
|
||||
join(
|
||||
'src',
|
||||
'wasm-lib',
|
||||
'tests',
|
||||
'executor',
|
||||
'inputs',
|
||||
'focusrite_scarlett_mounting_braket.kcl'
|
||||
),
|
||||
join(dir, 'bracket', 'main.kcl')
|
||||
)
|
||||
},
|
||||
})
|
||||
@ -64,8 +67,6 @@ test(
|
||||
|
||||
page.on('console', console.log)
|
||||
|
||||
page.on('console', console.log)
|
||||
|
||||
// expect to see the text bracket
|
||||
await expect(page.getByText('bracket')).toBeVisible()
|
||||
|
||||
@ -92,17 +93,20 @@ test(
|
||||
'when code with error first loads you get errors in console',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
await fsp.mkdir(`${dir}/broken-code`, { recursive: true })
|
||||
await fsp.mkdir(join(dir, 'broken-code'), { recursive: true })
|
||||
await fsp.copyFile(
|
||||
'src/wasm-lib/tests/executor/inputs/broken-code-test.kcl',
|
||||
`${dir}/broken-code/main.kcl`
|
||||
join(
|
||||
'src',
|
||||
'wasm-lib',
|
||||
'tests',
|
||||
'executor',
|
||||
'inputs',
|
||||
'broken-code-test.kcl'
|
||||
),
|
||||
join(dir, 'broken-code', 'main.kcl')
|
||||
)
|
||||
},
|
||||
})
|
||||
@ -232,10 +236,6 @@ test(
|
||||
'Rename and delete projects, also spam arrow keys when renaming',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
@ -524,10 +524,6 @@ test(
|
||||
'Deleting projects, can delete individual project, can still create projects after deleting all',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
})
|
||||
@ -622,10 +618,6 @@ test(
|
||||
'Can sort projects on home page',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
})
|
||||
@ -748,10 +740,6 @@ test(
|
||||
'When the project folder is empty, user can create new project and open it.',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const { electronApp, page } = await setupElectron({ testInfo })
|
||||
const u = await getUtils(page)
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
@ -836,25 +824,35 @@ test(
|
||||
'Opening a project should successfully load the stream, (regression test that this also works when switching between projects)',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async (dir) => {
|
||||
await Promise.all([
|
||||
fsp.mkdir(`${dir}/router-template-slate`, { recursive: true }),
|
||||
fsp.mkdir(`${dir}/bracket`, { recursive: true }),
|
||||
fsp.mkdir(join(dir, 'router-template-slate'), { recursive: true }),
|
||||
fsp.mkdir(join(dir, 'bracket'), { recursive: true }),
|
||||
])
|
||||
await Promise.all([
|
||||
fsp.copyFile(
|
||||
'src/wasm-lib/tests/executor/inputs/router-template-slate.kcl',
|
||||
`${dir}/router-template-slate/main.kcl`
|
||||
join(
|
||||
'src',
|
||||
'wasm-lib',
|
||||
'tests',
|
||||
'executor',
|
||||
'inputs',
|
||||
'router-template-slate.kcl'
|
||||
),
|
||||
join(dir, 'router-template-slate', 'main.kcl')
|
||||
),
|
||||
fsp.copyFile(
|
||||
'src/wasm-lib/tests/executor/inputs/focusrite_scarlett_mounting_braket.kcl',
|
||||
`${dir}/bracket/main.kcl`
|
||||
join(
|
||||
'src',
|
||||
'wasm-lib',
|
||||
'tests',
|
||||
'executor',
|
||||
'inputs',
|
||||
'focusrite_scarlett_mounting_braket.kcl'
|
||||
),
|
||||
join(dir, 'bracket', 'main.kcl')
|
||||
),
|
||||
])
|
||||
},
|
||||
@ -1302,10 +1300,6 @@ test(
|
||||
'Settings persist across restarts',
|
||||
{ tag: '@electron' },
|
||||
async ({ browserName }, testInfo) => {
|
||||
test.skip(
|
||||
process.platform === 'win32',
|
||||
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
||||
)
|
||||
await test.step('We can change a user setting like theme', async () => {
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
@ -1682,6 +1676,7 @@ test.describe('Renaming in the file tree', () => {
|
||||
})
|
||||
|
||||
await test.step('Rename the folder', async () => {
|
||||
await page.waitForTimeout(60000)
|
||||
await folderToRename.click({ button: 'right' })
|
||||
await expect(renameMenuItem).toBeVisible()
|
||||
await renameMenuItem.click()
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 60 KiB After Width: | Height: | Size: 60 KiB |
@ -2,10 +2,11 @@ import {
|
||||
expect,
|
||||
Page,
|
||||
Download,
|
||||
TestInfo,
|
||||
BrowserContext,
|
||||
TestInfo,
|
||||
_electron as electron,
|
||||
Locator,
|
||||
test,
|
||||
} from '@playwright/test'
|
||||
import { EngineCommand } from 'lang/std/artifactGraph'
|
||||
import os from 'os'
|
||||
@ -44,6 +45,9 @@ export const commonPoints = {
|
||||
num2: 14.44,
|
||||
}
|
||||
|
||||
export const editorSelector = '[role="textbox"][data-language="kcl"]'
|
||||
type PaneId = 'variables' | 'code' | 'files' | 'logs'
|
||||
|
||||
async function waitForPageLoadWithRetry(page: Page) {
|
||||
await expect(async () => {
|
||||
await page.goto('/')
|
||||
@ -311,13 +315,19 @@ export function normaliseKclNumbers(code: string, ignoreZero = true): string {
|
||||
return replaceNumbers(code)
|
||||
}
|
||||
|
||||
export async function getUtils(page: Page) {
|
||||
export async function getUtils(page: Page, test_?: typeof test) {
|
||||
if (!test) {
|
||||
console.warn(
|
||||
'Some methods in getUtils requires test object as second argument'
|
||||
)
|
||||
}
|
||||
|
||||
// Chrome devtools protocol session only works in Chromium
|
||||
const browserType = page.context().browser()?.browserType().name()
|
||||
const cdpSession =
|
||||
browserType !== 'chromium' ? null : await page.context().newCDPSession(page)
|
||||
|
||||
return {
|
||||
const util = {
|
||||
waitForAuthSkipAppStart: () => waitForAuthAndLsp(page),
|
||||
waitForPageLoad: () => waitForPageLoad(page),
|
||||
waitForPageLoadWithRetry: () => waitForPageLoadWithRetry(page),
|
||||
@ -484,7 +494,74 @@ export async function getUtils(page: Page) {
|
||||
networkOptions
|
||||
)
|
||||
},
|
||||
|
||||
toNormalizedCode: (text: string) => {
|
||||
return text.replace(/\s+/g, '')
|
||||
},
|
||||
|
||||
createAndSelectProject: async (hasText: string) => {
|
||||
return test_?.step(
|
||||
`Create and select project with text "${hasText}"`,
|
||||
async () => {
|
||||
await page.getByTestId('home-new-file').click()
|
||||
const projectLinksPost = page.getByTestId('project-link')
|
||||
await projectLinksPost.filter({ hasText }).click()
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
editorTextMatches: async (code: string) => {
|
||||
const editor = page.locator(editorSelector)
|
||||
const editorText = await editor.textContent()
|
||||
return expect(util.toNormalizedCode(editorText || '')).toBe(
|
||||
util.toNormalizedCode(code)
|
||||
)
|
||||
},
|
||||
|
||||
pasteCodeInEditor: async (code: string) => {
|
||||
return test?.step('Paste in KCL code', async () => {
|
||||
const editor = page.locator(editorSelector)
|
||||
await editor.fill(code)
|
||||
await util.editorTextMatches(code)
|
||||
})
|
||||
},
|
||||
|
||||
clickPane: async (paneId: PaneId) => {
|
||||
return test?.step(`Open ${paneId} pane`, async () => {
|
||||
await page.getByTestId(paneId + '-pane-button').click()
|
||||
await expect(page.locator('#' + paneId + '-pane')).toBeVisible()
|
||||
})
|
||||
},
|
||||
|
||||
createNewFileAndSelect: async (name: string) => {
|
||||
return test?.step(`Create a file named ${name}, select it`, async () => {
|
||||
await page.getByTestId('create-file-button').click()
|
||||
await page.getByTestId('file-rename-field').fill(name)
|
||||
await page.keyboard.press('Enter')
|
||||
await page
|
||||
.getByTestId('file-pane-scroll-container')
|
||||
.filter({ hasText: name })
|
||||
.click()
|
||||
})
|
||||
},
|
||||
|
||||
panesOpen: async (paneIds: PaneId[]) => {
|
||||
return test?.step(`Setting ${paneIds} panes to be open`, async () => {
|
||||
await page.addInitScript(
|
||||
({ PERSIST_MODELING_CONTEXT, paneIds }) => {
|
||||
localStorage.setItem(
|
||||
PERSIST_MODELING_CONTEXT,
|
||||
JSON.stringify({ openPanes: paneIds })
|
||||
)
|
||||
},
|
||||
{ PERSIST_MODELING_CONTEXT, paneIds }
|
||||
)
|
||||
await page.reload()
|
||||
})
|
||||
},
|
||||
}
|
||||
|
||||
return util
|
||||
}
|
||||
|
||||
type TemplateOptions = Array<number | Array<number>>
|
||||
@ -733,6 +810,7 @@ export async function setup(context: BrowserContext, page: Page) {
|
||||
// kill animations, speeds up tests and reduced flakiness
|
||||
await page.emulateMedia({ reducedMotion: 'reduce' })
|
||||
|
||||
// Trigger a navigation, since loading file:// doesn't.
|
||||
await page.reload()
|
||||
}
|
||||
|
||||
|
@ -264,4 +264,69 @@ test.describe('Testing settings', () => {
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
`Closing settings modal should go back to the original file being viewed`,
|
||||
{ tag: '@electron' },
|
||||
async ({ browser: _ }, testInfo) => {
|
||||
const { electronApp, page } = await setupElectron({
|
||||
testInfo,
|
||||
folderSetupFn: async () => {},
|
||||
})
|
||||
|
||||
const {
|
||||
panesOpen,
|
||||
createAndSelectProject,
|
||||
pasteCodeInEditor,
|
||||
clickPane,
|
||||
createNewFileAndSelect,
|
||||
editorTextMatches,
|
||||
} = await getUtils(page, test)
|
||||
|
||||
await page.setViewportSize({ width: 1200, height: 500 })
|
||||
page.on('console', console.log)
|
||||
|
||||
await panesOpen([])
|
||||
|
||||
await test.step('Precondition: No projects exist', async () => {
|
||||
await expect(page.getByTestId('home-section')).toBeVisible()
|
||||
const projectLinksPre = page.getByTestId('project-link')
|
||||
await expect(projectLinksPre).toHaveCount(0)
|
||||
})
|
||||
|
||||
await createAndSelectProject('project-000')
|
||||
|
||||
await clickPane('code')
|
||||
const kclCube = await fsp.readFile(
|
||||
'src/wasm-lib/tests/executor/inputs/cube.kcl',
|
||||
'utf-8'
|
||||
)
|
||||
await pasteCodeInEditor(kclCube)
|
||||
|
||||
await clickPane('files')
|
||||
await createNewFileAndSelect('2.kcl')
|
||||
|
||||
const kclCylinder = await fsp.readFile(
|
||||
'src/wasm-lib/tests/executor/inputs/cylinder.kcl',
|
||||
'utf-8'
|
||||
)
|
||||
await pasteCodeInEditor(kclCylinder)
|
||||
|
||||
const settingsOpenButton = page.getByRole('link', {
|
||||
name: 'settings Settings',
|
||||
})
|
||||
const settingsCloseButton = page.getByTestId('settings-close-button')
|
||||
|
||||
await test.step('Open and close settings', async () => {
|
||||
await settingsOpenButton.click()
|
||||
await settingsCloseButton.click()
|
||||
})
|
||||
|
||||
await test.step('Postcondition: Same file content is in editor as before settings opened', async () => {
|
||||
await editorTextMatches(kclCylinder)
|
||||
})
|
||||
|
||||
await electronApp.close()
|
||||
}
|
||||
)
|
||||
})
|
||||
|
@ -83,7 +83,7 @@
|
||||
"fetch:wasm": "./get-latest-wasm-bundle.sh",
|
||||
"isomorphic-copy-wasm": "(copy src/wasm-lib/pkg/wasm_lib_bg.wasm public || cp src/wasm-lib/pkg/wasm_lib_bg.wasm public)",
|
||||
"build:wasm-dev": "(cd src/wasm-lib && wasm-pack build --dev --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && yarn isomorphic-copy-wasm && yarn fmt",
|
||||
"build:wasm": "(cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings) && yarn isomorphic-copy-wasm && yarn fmt",
|
||||
"build:wasm": "cd src/wasm-lib && wasm-pack build --release --target web --out-dir pkg && cargo test -p kcl-lib export_bindings && cd ../.. && yarn isomorphic-copy-wasm && yarn fmt",
|
||||
"build:wasm-clean": "yarn wasm-prep && yarn build:wasm",
|
||||
"remove-importmeta": "sed -i 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\"; sed -i '' 's/import.meta.url/window.location.origin/g' \"./src/wasm-lib/pkg/wasm_lib.js\" || echo \"sed for both mac and linux\"",
|
||||
"wasm-prep": "rm -rf src/wasm-lib/pkg && mkdir src/wasm-lib/pkg && rm -rf src/wasm-lib/kcl/bindings",
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { defineConfig } from '@playwright/test'
|
||||
import { defineConfig, devices } from '@playwright/test'
|
||||
|
||||
/**
|
||||
* See https://playwright.dev/docs/test-configuration.
|
||||
@ -30,4 +30,24 @@ export default defineConfig({
|
||||
actionTimeout: 15_000,
|
||||
screenshot: 'only-on-failure',
|
||||
},
|
||||
projects: [
|
||||
{
|
||||
name: 'Google Chrome',
|
||||
use: {
|
||||
...devices['Desktop Chrome'],
|
||||
channel: 'chrome',
|
||||
contextOptions: {
|
||||
/* Chromium is the only one with these permission types */
|
||||
permissions: ['clipboard-write', 'clipboard-read'],
|
||||
},
|
||||
launchOptions: {
|
||||
...(process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH
|
||||
? {
|
||||
executablePath: process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH,
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
}, // or 'chrome-beta'
|
||||
},
|
||||
],
|
||||
})
|
||||
|
@ -61,6 +61,7 @@ function RenameForm({
|
||||
<label>
|
||||
<span className="sr-only">Rename file</span>
|
||||
<input
|
||||
data-testid="file-rename-field"
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
autoFocus
|
||||
@ -402,6 +403,7 @@ export const FileTreeMenu = () => {
|
||||
<>
|
||||
<ActionButton
|
||||
Element="button"
|
||||
data-testid="create-file-button"
|
||||
iconStart={{
|
||||
icon: 'filePlus',
|
||||
iconClassName: '!text-current',
|
||||
@ -417,6 +419,7 @@ export const FileTreeMenu = () => {
|
||||
|
||||
<ActionButton
|
||||
Element="button"
|
||||
data-testid="create-folder-button"
|
||||
iconStart={{
|
||||
icon: 'folderPlus',
|
||||
iconClassName: '!text-current',
|
||||
|
@ -16,7 +16,6 @@ import init, {
|
||||
parse_app_settings,
|
||||
parse_project_settings,
|
||||
default_project_settings,
|
||||
parse_project_route,
|
||||
base64_decode,
|
||||
} from '../wasm-lib/pkg/wasm_lib'
|
||||
import { KCLError } from './errors'
|
||||
@ -33,7 +32,6 @@ import { CoreDumpManager } from 'lib/coredump'
|
||||
import openWindow from 'lib/openWindow'
|
||||
import { DefaultPlanes } from 'wasm-lib/kcl/bindings/DefaultPlanes'
|
||||
import { TEST } from 'env'
|
||||
import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute'
|
||||
import { err } from 'lib/trap'
|
||||
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
||||
import { DeepPartial } from 'lib/types'
|
||||
@ -611,13 +609,6 @@ export function parseProjectSettings(
|
||||
return parse_project_settings(toml)
|
||||
}
|
||||
|
||||
export function parseProjectRoute(
|
||||
configuration: DeepPartial<Configuration>,
|
||||
route_str: string
|
||||
): ProjectRoute | Error {
|
||||
return parse_project_route(JSON.stringify(configuration), route_str)
|
||||
}
|
||||
|
||||
export function base64Decode(base64: string): ArrayBuffer | Error {
|
||||
try {
|
||||
const decoded = base64_decode(base64)
|
||||
|
@ -19,7 +19,6 @@ import {
|
||||
import { DeepPartial } from './types'
|
||||
import { ProjectConfiguration } from 'wasm-lib/kcl/bindings/ProjectConfiguration'
|
||||
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
||||
export { parseProjectRoute } from 'lang/wasm'
|
||||
|
||||
export async function renameProjectDirectory(
|
||||
projectPath: string,
|
||||
@ -39,7 +38,7 @@ export async function renameProjectDirectory(
|
||||
|
||||
// Make sure the new name does not exist.
|
||||
const newPath = window.electron.path.join(
|
||||
projectPath.split('/').slice(0, -1).join('/'),
|
||||
window.electron.path.dirname(projectPath),
|
||||
newName
|
||||
)
|
||||
try {
|
||||
@ -186,9 +185,9 @@ const collectAllFilesRecursiveFrom = async (path: string) => {
|
||||
return Promise.reject(new Error(`Path ${path} is not a directory`))
|
||||
}
|
||||
|
||||
const pathParts = path.split(window.electron.path.sep)
|
||||
const name = window.electron.path.basename(path)
|
||||
let entry: FileEntry = {
|
||||
name: pathParts.slice(-1)[0],
|
||||
name: name,
|
||||
path,
|
||||
children: [],
|
||||
}
|
||||
@ -330,7 +329,6 @@ export async function getProjectInfo(projectPath: string): Promise<Project> {
|
||||
new Error(`Project path is not a directory: ${projectPath}`)
|
||||
)
|
||||
}
|
||||
|
||||
let walked = await collectAllFilesRecursiveFrom(projectPath)
|
||||
let default_file = await getDefaultKclFileForDir(projectPath, walked)
|
||||
const metadata = await window.electron.stat(projectPath)
|
||||
|
79
src/lib/paths.test.ts
Normal file
79
src/lib/paths.test.ts
Normal file
@ -0,0 +1,79 @@
|
||||
import { parseProjectRoute } from './paths'
|
||||
import * as path from 'path'
|
||||
|
||||
describe('testing parseProjectRoute', () => {
|
||||
it('should parse a project as a subpath of project dir', async () => {
|
||||
let config = {
|
||||
settings: {
|
||||
project: {
|
||||
directory: '/home/somebody/projects',
|
||||
},
|
||||
},
|
||||
}
|
||||
const route = '/home/somebody/projects/project'
|
||||
expect(await parseProjectRoute(config, route, path)).toEqual({
|
||||
projectName: 'project',
|
||||
projectPath: route,
|
||||
currentFileName: null,
|
||||
currentFilePath: null,
|
||||
})
|
||||
})
|
||||
it('should parse a project as the project dir', async () => {
|
||||
let config = {
|
||||
settings: {
|
||||
project: {
|
||||
directory: '/home/somebody/projects',
|
||||
},
|
||||
},
|
||||
}
|
||||
const route = '/home/somebody/projects'
|
||||
expect(await parseProjectRoute(config, route, path)).toEqual({
|
||||
projectName: null,
|
||||
projectPath: route,
|
||||
currentFileName: null,
|
||||
currentFilePath: null,
|
||||
})
|
||||
})
|
||||
it('should parse a project with file in the project dir', async () => {
|
||||
let config = {
|
||||
settings: {
|
||||
project: {
|
||||
directory: '/home/somebody/projects',
|
||||
},
|
||||
},
|
||||
}
|
||||
const route = '/home/somebody/projects/assembly/main.kcl'
|
||||
expect(await parseProjectRoute(config, route, path)).toEqual({
|
||||
projectName: 'assembly',
|
||||
projectPath: '/home/somebody/projects/assembly',
|
||||
currentFileName: 'main.kcl',
|
||||
currentFilePath: route,
|
||||
})
|
||||
})
|
||||
it('should parse a project with file in a subdir in the project dir', async () => {
|
||||
let config = {
|
||||
settings: {
|
||||
project: {
|
||||
directory: '/home/somebody/projects',
|
||||
},
|
||||
},
|
||||
}
|
||||
const route = '/home/somebody/projects/assembly/subdir/main.kcl'
|
||||
expect(await parseProjectRoute(config, route, path)).toEqual({
|
||||
projectName: 'assembly',
|
||||
projectPath: '/home/somebody/projects/assembly',
|
||||
currentFileName: 'main.kcl',
|
||||
currentFilePath: route,
|
||||
})
|
||||
})
|
||||
it('should work in the browser context', async () => {
|
||||
let config = {}
|
||||
const route = '/browser/main.kcl'
|
||||
expect(await parseProjectRoute(config, route, undefined)).toEqual({
|
||||
projectName: 'browser',
|
||||
projectPath: '/browser',
|
||||
currentFileName: 'main.kcl',
|
||||
currentFilePath: route,
|
||||
})
|
||||
})
|
||||
})
|
@ -1,13 +1,13 @@
|
||||
import { onboardingPaths } from 'routes/Onboarding/paths'
|
||||
import { BROWSER_FILE_NAME, BROWSER_PROJECT_NAME, FILE_EXT } from './constants'
|
||||
import { isDesktop } from './isDesktop'
|
||||
import { ProjectRoute } from 'wasm-lib/kcl/bindings/ProjectRoute'
|
||||
import { parseProjectRoute, readAppSettingsFile } from './desktop'
|
||||
import { readAppSettingsFile } from './desktop'
|
||||
import { readLocalStorageAppSettingsFile } from './settings/settingsUtils'
|
||||
import { err } from 'lib/trap'
|
||||
import { IS_PLAYWRIGHT_KEY } from '../../e2e/playwright/storageStates'
|
||||
import { DeepPartial } from './types'
|
||||
import { Configuration } from 'wasm-lib/kcl/bindings/Configuration'
|
||||
import { PlatformPath } from 'path'
|
||||
|
||||
const prependRoutes =
|
||||
(routesObject: Record<string, string>) => (prepend: string) => {
|
||||
@ -25,6 +25,13 @@ type OnboardingPaths = {
|
||||
|
||||
const SETTINGS = '/settings' as const
|
||||
|
||||
export type ProjectRoute = {
|
||||
projectName: string | null
|
||||
projectPath: string
|
||||
currentFileName: string | null
|
||||
currentFilePath: string | null
|
||||
}
|
||||
|
||||
export const PATHS = {
|
||||
INDEX: '/',
|
||||
HOME: '/home',
|
||||
@ -60,9 +67,64 @@ export async function getProjectMetaByRouteId(
|
||||
return Promise.reject(new Error('No configuration found'))
|
||||
}
|
||||
|
||||
const route = parseProjectRoute(configuration, id)
|
||||
const route = parseProjectRoute(configuration, id, window?.electron?.path)
|
||||
|
||||
if (err(route)) return Promise.reject(route)
|
||||
|
||||
return route
|
||||
}
|
||||
|
||||
export async function parseProjectRoute(
|
||||
configuration: DeepPartial<Configuration>,
|
||||
id: string,
|
||||
pathlib: PlatformPath | undefined
|
||||
): Promise<ProjectRoute> {
|
||||
let projectName = null
|
||||
let projectPath = ''
|
||||
let currentFileName = null
|
||||
let currentFilePath = null
|
||||
if (
|
||||
pathlib &&
|
||||
configuration.settings?.project?.directory &&
|
||||
id.startsWith(configuration.settings.project.directory)
|
||||
) {
|
||||
const relativeToRoot = pathlib.relative(
|
||||
configuration.settings.project.directory,
|
||||
id
|
||||
)
|
||||
projectName = relativeToRoot.split(pathlib.sep)[0]
|
||||
projectPath = pathlib.join(
|
||||
configuration.settings.project.directory,
|
||||
projectName
|
||||
)
|
||||
projectName = projectName === '' ? null : projectName
|
||||
} else {
|
||||
projectPath = id
|
||||
if (pathlib) {
|
||||
if (pathlib.extname(id) === '.kcl') {
|
||||
projectPath = pathlib.dirname(id)
|
||||
}
|
||||
projectName = pathlib.basename(projectPath)
|
||||
} else {
|
||||
if (id.endsWith('.kcl')) {
|
||||
projectPath = '/browser'
|
||||
projectName = 'browser'
|
||||
}
|
||||
}
|
||||
}
|
||||
if (pathlib) {
|
||||
if (projectPath !== id) {
|
||||
currentFileName = pathlib.basename(id)
|
||||
currentFilePath = id
|
||||
}
|
||||
} else {
|
||||
currentFileName = 'main.kcl'
|
||||
currentFilePath = id
|
||||
}
|
||||
return {
|
||||
projectName: projectName,
|
||||
projectPath: projectPath,
|
||||
currentFileName: currentFileName,
|
||||
currentFilePath: currentFilePath,
|
||||
}
|
||||
}
|
||||
|
@ -36,9 +36,9 @@ export const settingsLoader: LoaderFunction = async ({
|
||||
configuration
|
||||
)
|
||||
if (projectPathData) {
|
||||
const { project_path } = projectPathData
|
||||
const { projectPath } = projectPathData
|
||||
const { settings: s } = await loadAndValidateSettings(
|
||||
project_path || undefined
|
||||
projectPath || undefined
|
||||
)
|
||||
return s
|
||||
}
|
||||
@ -83,48 +83,49 @@ export const fileLoader: LoaderFunction = async (
|
||||
const isBrowserProject = params.id === decodeURIComponent(BROWSER_PATH)
|
||||
|
||||
if (!isBrowserProject && projectPathData) {
|
||||
const { project_name, project_path, current_file_name, current_file_path } =
|
||||
const { projectName, projectPath, currentFileName, currentFilePath } =
|
||||
projectPathData
|
||||
|
||||
const urlObj = new URL(routerData.request.url)
|
||||
let code = ''
|
||||
|
||||
if (!urlObj.pathname.endsWith('/settings')) {
|
||||
if (!current_file_name || !current_file_path || !project_name) {
|
||||
if (!currentFileName || !currentFilePath || !projectName) {
|
||||
return redirect(
|
||||
`${PATHS.FILE}/${encodeURIComponent(
|
||||
isDesktop()
|
||||
? (await getProjectInfo(project_path)).default_file
|
||||
? (await getProjectInfo(projectPath)).default_file
|
||||
: params.id + '/' + PROJECT_ENTRYPOINT
|
||||
)}`
|
||||
)
|
||||
}
|
||||
|
||||
code = await window.electron.readFile(current_file_path)
|
||||
code = await window.electron.readFile(currentFilePath)
|
||||
code = normalizeLineEndings(code)
|
||||
|
||||
// Update both the state and the editor's code.
|
||||
// We explicitly do not write to the file here since we are loading from
|
||||
// the file system and not the editor.
|
||||
codeManager.updateCurrentFilePath(current_file_path)
|
||||
codeManager.updateCurrentFilePath(currentFilePath)
|
||||
codeManager.updateCodeStateEditor(code)
|
||||
}
|
||||
|
||||
// Set the file system manager to the project path
|
||||
// So that WASM gets an updated path for operations
|
||||
fileSystemManager.dir = project_path
|
||||
fileSystemManager.dir = projectPath
|
||||
|
||||
const defaultProjectData = {
|
||||
name: project_name || 'unnamed',
|
||||
path: project_path,
|
||||
name: projectName || 'unnamed',
|
||||
path: projectPath,
|
||||
children: [],
|
||||
kcl_file_count: 0,
|
||||
directory_count: 0,
|
||||
metadata: null,
|
||||
default_file: project_path,
|
||||
default_file: projectPath,
|
||||
}
|
||||
|
||||
const maybeProjectInfo = isDesktop()
|
||||
? await getProjectInfo(project_path)
|
||||
? await getProjectInfo(projectPath)
|
||||
: null
|
||||
|
||||
console.log('maybeProjectInfo', {
|
||||
@ -137,8 +138,8 @@ export const fileLoader: LoaderFunction = async (
|
||||
code,
|
||||
project: maybeProjectInfo ?? defaultProjectData,
|
||||
file: {
|
||||
name: current_file_name || '',
|
||||
path: current_file_path || '',
|
||||
name: currentFileName || '',
|
||||
path: currentFilePath || '',
|
||||
children: [],
|
||||
},
|
||||
}
|
||||
@ -187,3 +188,7 @@ export const homeLoader: LoaderFunction = async (): Promise<
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const normalizeLineEndings = (str: string, normalized = '\n') => {
|
||||
return str.replace(/\r?\n/g, normalized)
|
||||
}
|
||||
|
@ -8,8 +8,6 @@ use parse_display::{Display, FromStr};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::settings::types::Configuration;
|
||||
|
||||
/// State management for the application.
|
||||
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
|
||||
#[ts(export)]
|
||||
@ -148,99 +146,6 @@ const model = import("{}")"#,
|
||||
}
|
||||
}
|
||||
|
||||
/// Project route information.
|
||||
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
|
||||
#[ts(export)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct ProjectRoute {
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub project_name: Option<String>,
|
||||
pub project_path: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub current_file_name: Option<String>,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub current_file_path: Option<String>,
|
||||
}
|
||||
|
||||
impl ProjectRoute {
|
||||
/// Get the project state from the url in the route.
|
||||
pub fn from_route(configuration: &Configuration, route: &str) -> Result<Self> {
|
||||
let path = std::path::Path::new(route);
|
||||
// Check if the default project path is in the route.
|
||||
let (project_path, project_name) = if path.starts_with(&configuration.settings.project.directory)
|
||||
&& configuration.settings.project.directory != std::path::PathBuf::default()
|
||||
{
|
||||
// Get the project name.
|
||||
if let Some(project_name) = path
|
||||
.strip_prefix(&configuration.settings.project.directory)?
|
||||
.iter()
|
||||
.next()
|
||||
{
|
||||
(
|
||||
configuration
|
||||
.settings
|
||||
.project
|
||||
.directory
|
||||
.join(project_name)
|
||||
.display()
|
||||
.to_string(),
|
||||
Some(project_name.to_string_lossy().to_string()),
|
||||
)
|
||||
} else {
|
||||
(configuration.settings.project.directory.display().to_string(), None)
|
||||
}
|
||||
} else {
|
||||
// Assume the project path is the parent directory of the file.
|
||||
let project_dir = if path.display().to_string().ends_with(".kcl") {
|
||||
path.parent()
|
||||
.ok_or_else(|| anyhow::anyhow!("Parent directory not found: {}", path.display()))?
|
||||
} else {
|
||||
path
|
||||
};
|
||||
|
||||
if project_dir == std::path::Path::new("/") {
|
||||
(
|
||||
path.display().to_string(),
|
||||
Some(
|
||||
path.file_name()
|
||||
.ok_or_else(|| anyhow::anyhow!("File name not found: {}", path.display()))?
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
),
|
||||
)
|
||||
} else if let Some(project_name) = project_dir.file_name() {
|
||||
(
|
||||
project_dir.display().to_string(),
|
||||
Some(project_name.to_string_lossy().to_string()),
|
||||
)
|
||||
} else {
|
||||
(project_dir.display().to_string(), None)
|
||||
}
|
||||
};
|
||||
|
||||
let (current_file_name, current_file_path) = if path.display().to_string() == project_path {
|
||||
(None, None)
|
||||
} else {
|
||||
(
|
||||
Some(
|
||||
path.file_name()
|
||||
.ok_or_else(|| anyhow::anyhow!("File name not found: {}", path.display()))?
|
||||
.to_string_lossy()
|
||||
.to_string(),
|
||||
),
|
||||
Some(path.display().to_string()),
|
||||
)
|
||||
};
|
||||
|
||||
Ok(Self {
|
||||
project_name,
|
||||
project_path,
|
||||
current_file_name,
|
||||
current_file_path,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Information about project.
|
||||
#[derive(Debug, Default, Clone, Deserialize, Serialize, JsonSchema, ts_rs::TS, PartialEq)]
|
||||
#[ts(export)]
|
||||
@ -535,160 +440,6 @@ impl From<std::fs::Metadata> for FileMetadata {
|
||||
mod tests {
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn test_project_route_from_route_std_path() {
|
||||
let mut configuration = crate::settings::types::Configuration::default();
|
||||
configuration.settings.project.directory =
|
||||
std::path::PathBuf::from("/Users/macinatormax/Documents/kittycad-modeling-projects");
|
||||
|
||||
let route = "/Users/macinatormax/Documents/kittycad-modeling-projects/assembly/main.kcl";
|
||||
let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
|
||||
assert_eq!(
|
||||
state,
|
||||
super::ProjectRoute {
|
||||
project_name: Some("assembly".to_string()),
|
||||
project_path: "/Users/macinatormax/Documents/kittycad-modeling-projects/assembly".to_string(),
|
||||
current_file_name: Some("main.kcl".to_string()),
|
||||
current_file_path: Some(
|
||||
"/Users/macinatormax/Documents/kittycad-modeling-projects/assembly/main.kcl".to_string()
|
||||
),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_project_route_from_route_std_path_dir() {
|
||||
let mut configuration = crate::settings::types::Configuration::default();
|
||||
configuration.settings.project.directory =
|
||||
std::path::PathBuf::from("/Users/macinatormax/Documents/kittycad-modeling-projects");
|
||||
|
||||
let route = "/Users/macinatormax/Documents/kittycad-modeling-projects/assembly";
|
||||
let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
|
||||
assert_eq!(
|
||||
state,
|
||||
super::ProjectRoute {
|
||||
project_name: Some("assembly".to_string()),
|
||||
project_path: "/Users/macinatormax/Documents/kittycad-modeling-projects/assembly".to_string(),
|
||||
current_file_name: None,
|
||||
current_file_path: None
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_project_route_from_route_std_path_dir_empty() {
|
||||
let mut configuration = crate::settings::types::Configuration::default();
|
||||
configuration.settings.project.directory =
|
||||
std::path::PathBuf::from("/Users/macinatormax/Documents/kittycad-modeling-projects");
|
||||
|
||||
let route = "/Users/macinatormax/Documents/kittycad-modeling-projects";
|
||||
let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
|
||||
assert_eq!(
|
||||
state,
|
||||
super::ProjectRoute {
|
||||
project_name: None,
|
||||
project_path: "/Users/macinatormax/Documents/kittycad-modeling-projects".to_string(),
|
||||
current_file_name: None,
|
||||
current_file_path: None
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_project_route_from_route_outside_std_path() {
|
||||
let mut configuration = crate::settings::types::Configuration::default();
|
||||
configuration.settings.project.directory =
|
||||
std::path::PathBuf::from("/Users/macinatormax/Documents/kittycad-modeling-projects");
|
||||
|
||||
let route = "/Users/macinatormax/kittycad/modeling-app/main.kcl";
|
||||
let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
|
||||
assert_eq!(
|
||||
state,
|
||||
super::ProjectRoute {
|
||||
project_name: Some("modeling-app".to_string()),
|
||||
project_path: "/Users/macinatormax/kittycad/modeling-app".to_string(),
|
||||
current_file_name: Some("main.kcl".to_string()),
|
||||
current_file_path: Some("/Users/macinatormax/kittycad/modeling-app/main.kcl".to_string()),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_project_route_from_route_outside_std_path_dir() {
|
||||
let mut configuration = crate::settings::types::Configuration::default();
|
||||
configuration.settings.project.directory =
|
||||
std::path::PathBuf::from("/Users/macinatormax/Documents/kittycad-modeling-projects");
|
||||
|
||||
let route = "/Users/macinatormax/kittycad/modeling-app";
|
||||
let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
|
||||
assert_eq!(
|
||||
state,
|
||||
super::ProjectRoute {
|
||||
project_name: Some("modeling-app".to_string()),
|
||||
project_path: "/Users/macinatormax/kittycad/modeling-app".to_string(),
|
||||
current_file_name: None,
|
||||
current_file_path: None
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_project_route_from_route_browser() {
|
||||
let mut configuration = crate::settings::types::Configuration::default();
|
||||
configuration.settings.project.directory = std::path::PathBuf::default();
|
||||
|
||||
let route = "/browser/main.kcl";
|
||||
let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
|
||||
assert_eq!(
|
||||
state,
|
||||
super::ProjectRoute {
|
||||
project_name: Some("browser".to_string()),
|
||||
project_path: "/browser".to_string(),
|
||||
current_file_name: Some("main.kcl".to_string()),
|
||||
current_file_path: Some("/browser/main.kcl".to_string())
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_project_route_from_route_browser_no_path() {
|
||||
let mut configuration = crate::settings::types::Configuration::default();
|
||||
configuration.settings.project.directory = std::path::PathBuf::default();
|
||||
|
||||
let route = "/browser";
|
||||
let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
|
||||
assert_eq!(
|
||||
state,
|
||||
super::ProjectRoute {
|
||||
project_name: Some("browser".to_string()),
|
||||
project_path: "/browser".to_string(),
|
||||
current_file_name: None,
|
||||
current_file_path: None
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_project_route_from_route_non_main_file() {
|
||||
let mut configuration = crate::settings::types::Configuration::default();
|
||||
configuration.settings.project.directory =
|
||||
std::path::PathBuf::from("/Users/macinatormax/Documents/kittycad-modeling-projects");
|
||||
|
||||
let route = "/Users/macinatormax/Documents/kittycad-modeling-projects/assembly/thing.kcl";
|
||||
let state = super::ProjectRoute::from_route(&configuration, route).unwrap();
|
||||
assert_eq!(
|
||||
state,
|
||||
super::ProjectRoute {
|
||||
project_name: Some("assembly".to_string()),
|
||||
project_path: "/Users/macinatormax/Documents/kittycad-modeling-projects/assembly".to_string(),
|
||||
current_file_name: Some("thing.kcl".to_string()),
|
||||
current_file_path: Some(
|
||||
"/Users/macinatormax/Documents/kittycad-modeling-projects/assembly/thing.kcl".to_string()
|
||||
),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_default_kcl_file_for_dir_non_exist() {
|
||||
let name = format!("kittycad-modeling-projects-{}", uuid::Uuid::new_v4());
|
||||
|
@ -566,22 +566,6 @@ pub fn serialize_project_settings(val: JsValue) -> Result<JsValue, String> {
|
||||
Ok(JsValue::from_str(&toml_str))
|
||||
}
|
||||
|
||||
/// Parse the project route.
|
||||
#[wasm_bindgen]
|
||||
pub fn parse_project_route(configuration: &str, route: &str) -> Result<JsValue, String> {
|
||||
console_error_panic_hook::set_once();
|
||||
|
||||
let configuration: kcl_lib::settings::types::Configuration =
|
||||
serde_json::from_str(configuration).map_err(|e| e.to_string())?;
|
||||
|
||||
let route =
|
||||
kcl_lib::settings::types::file::ProjectRoute::from_route(&configuration, route).map_err(|e| e.to_string())?;
|
||||
|
||||
// The serde-wasm-bindgen does not work here because of weird HashMap issues so we use the
|
||||
// gloo-serialize crate instead.
|
||||
JsValue::from_serde(&route).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
static ALLOWED_DECODING_FORMATS: &[data_encoding::Encoding] = &[
|
||||
data_encoding::BASE64,
|
||||
data_encoding::BASE64URL,
|
||||
|
Reference in New Issue
Block a user