Skip to content

Commit

Permalink
add tests
Browse files Browse the repository at this point in the history
Signed-off-by: Jess Frazelle <github@jessfraz.com>
  • Loading branch information
jessfraz committed Aug 24, 2024
1 parent 5fc4884 commit 6c239d2
Show file tree
Hide file tree
Showing 5 changed files with 230 additions and 12 deletions.
11 changes: 5 additions & 6 deletions src/Router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,13 @@ const router = createRouter([
loader: async () => {
const onDesktop = isDesktop()
if (onDesktop) {
const projectStartup = await window.electron.loadProjectAtStartup()
if (projectStartup !== null) {
const projectStartupFile =
await window.electron.loadProjectAtStartup()
if (projectStartupFile !== null) {
// Redirect to the file if we have a file path.
if (projectStartup.currentFile?.length > 0) {
if (projectStartupFile.length > 0) {
return redirect(
PATHS.FILE +
'/' +
encodeURIComponent(projectStartup.currentFile)
PATHS.FILE + '/' + encodeURIComponent(projectStartupFile)
)
}
}
Expand Down
92 changes: 92 additions & 0 deletions src/lib/getCurrentProjectFile.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
import { promises as fs } from 'fs'
import path from 'path'
import os from 'os'
import { v4 as uuidv4 } from 'uuid'
import getCurrentProjectFile from './getCurrentProjectFile'

describe('getCurrentProjectFile', () => {
test('with explicit open file with space (URL encoded)', async () => {
const name = `kittycad-modeling-projects-${uuidv4()}`
const tmpProjectDir = path.join(os.tmpdir(), name)

await fs.mkdir(tmpProjectDir, { recursive: true })
await fs.writeFile(path.join(tmpProjectDir, 'i have a space.kcl'), '')

const state = await getCurrentProjectFile(
path.join(tmpProjectDir, 'i%20have%20a%20space.kcl')
)

expect(state).toBe(path.join(tmpProjectDir, 'i have a space.kcl'))

await fs.rm(tmpProjectDir, { recursive: true, force: true })
})

test('with explicit open file with space', async () => {
const name = `kittycad-modeling-projects-${uuidv4()}`
const tmpProjectDir = path.join(os.tmpdir(), name)

await fs.mkdir(tmpProjectDir, { recursive: true })
await fs.writeFile(path.join(tmpProjectDir, 'i have a space.kcl'), '')

const state = await getCurrentProjectFile(
path.join(tmpProjectDir, 'i have a space.kcl')
)

expect(state).toBe(path.join(tmpProjectDir, 'i have a space.kcl'))

await fs.rm(tmpProjectDir, { recursive: true, force: true })
})

test('with source path dot', async () => {
const name = `kittycad-modeling-projects-${uuidv4()}`
const tmpProjectDir = path.join(os.tmpdir(), name)
await fs.mkdir(tmpProjectDir, { recursive: true })

// Set the current directory to the temp project directory.
const originalCwd = process.cwd()
process.chdir(tmpProjectDir)

try {
const state = await getCurrentProjectFile('.')

expect(state).toBe(path.join(tmpProjectDir, 'main.kcl'))
} finally {
process.chdir(originalCwd)
await fs.rm(tmpProjectDir, { recursive: true, force: true })
}
})

test('with main.kcl not existing', async () => {
const name = `kittycad-modeling-projects-${uuidv4()}`
const tmpProjectDir = path.join(os.tmpdir(), name)
await fs.mkdir(tmpProjectDir, { recursive: true })

try {
const state = await getCurrentProjectFile(tmpProjectDir)

expect(state).toBe(path.join(tmpProjectDir, 'main.kcl'))
} finally {
await fs.rm(tmpProjectDir, { recursive: true, force: true })
}
})

test('with directory, main.kcl not existing, other.kcl does', async () => {
const name = `kittycad-modeling-projects-${uuidv4()}`
const tmpProjectDir = path.join(os.tmpdir(), name)
await fs.mkdir(tmpProjectDir, { recursive: true })
await fs.writeFile(path.join(tmpProjectDir, 'other.kcl'), '')

try {
const state = await getCurrentProjectFile(tmpProjectDir)

expect(state).toBe(path.join(tmpProjectDir, 'other.kcl'))

// make sure we didn't create a main.kcl file
await expect(
fs.access(path.join(tmpProjectDir, 'main.kcl'))
).rejects.toThrow()
} finally {
await fs.rm(tmpProjectDir, { recursive: true, force: true })
}
})
})
116 changes: 116 additions & 0 deletions src/lib/getCurrentProjectFile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
import * as path from 'path'
import * as fs from 'fs/promises'
import { Models } from '@kittycad/lib/dist/types/src'
import { PROJECT_ENTRYPOINT } from './constants'

// Create a const object with the values
const FILE_IMPORT_FORMATS = {
fbx: 'fbx',
gltf: 'gltf',
obj: 'obj',
ply: 'ply',
sldprt: 'sldprt',
step: 'step',
stl: 'stl',
} as const

// Extract the values into an array
const fileImportFormats: Models['FileImportFormat_type'][] =
Object.values(FILE_IMPORT_FORMATS)
export const allFileImportFormats: string[] = [
...fileImportFormats,
'stp',
'fbxb',
'glb',
]
export const relevantExtensions = ['kcl', ...allFileImportFormats]

/// Get the current project file from the path.
/// This is used for double-clicking on a file in the file explorer,
/// or the command line args, or deep linking.
export default async function getCurrentProjectFile(
pathString: string
): Promise<string | Error> {
// Fix for "." path, which is the current directory.
let sourcePath = pathString === '.' ? process.cwd() : pathString

// URL decode the path.
sourcePath = decodeURIComponent(sourcePath)

// If the path does not start with a slash, it is a relative path.
// We need to convert it to an absolute path.
sourcePath = path.isAbsolute(sourcePath)
? sourcePath
: path.join(process.cwd(), sourcePath)

// If the path is a directory, let's assume it is a project directory.
const stats = await fs.stat(sourcePath)
if (stats.isDirectory()) {
// Walk the directory and look for a kcl file.
const files = await fs.readdir(sourcePath)
const kclFiles = files.filter((file) => path.extname(file) === '.kcl')

if (kclFiles.length === 0) {
let projectFile = path.join(sourcePath, PROJECT_ENTRYPOINT)
// Check if we have a main.kcl file in the project.
try {
await fs.access(projectFile)
} catch {
// Create the default file in the project.
await fs.writeFile(projectFile, '')
}

return projectFile
}

// If a project entrypoint file exists, use it.
// Otherwise, use the first kcl file in the project.
const gotMain = files.filter((file) => file === PROJECT_ENTRYPOINT)
if (gotMain.length === 0) {
return path.join(sourcePath, kclFiles[0])
}
return path.join(sourcePath, PROJECT_ENTRYPOINT)
}

// Check if the extension on what we are trying to open is a relevant file type.
const extension = path.extname(sourcePath).slice(1)

if (!relevantExtensions.includes(extension) && extension !== 'toml') {
return new Error(
`File type (${extension}) cannot be opened with this app: '${sourcePath}', try opening one of the following file types: ${relevantExtensions.join(
', '
)}`
)
}

// We were given a file path, not a directory.
// Let's get the parent directory of the file.
const parent = path.dirname(sourcePath)

// If we got an import model file, we need to check if we have a file in the project for
// this import model.
if (allFileImportFormats.includes(extension)) {
const importFileName = path.basename(sourcePath)
// Check if we have a file in the project for this import model.
const kclWrapperFilename = `${importFileName}.kcl`
const kclWrapperFilePath = path.join(parent, kclWrapperFilename)

try {
await fs.access(kclWrapperFilePath)
} catch {
// Create the file in the project with the default import content.
const content = `// This file was automatically generated by the application when you
// double-clicked on the model file.
// You can edit this file to add your own content.
// But we recommend you keep the import statement as it is.
// For more information on the import statement, see the documentation at:
// https://zoo.dev/docs/kcl/import
const model = import("${importFileName}")`
await fs.writeFile(kclWrapperFilePath, content)
}

return kclWrapperFilePath
}

return sourcePath
}
20 changes: 16 additions & 4 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Bonjour, Service } from 'bonjour-service'
// @ts-ignore: TS1343
import * as kittycad from '@kittycad/lib/import'
import minimist from 'minimist'
import { getProjectState } from '@kittycad/kcl.ts'
import getCurrentProjectFile from 'lib/getCurrentProjectFile'

// Check the command line arguments for a project path
const args = parseCLIArgs()
Expand Down Expand Up @@ -170,6 +170,13 @@ ipcMain.handle('find_machine_api', () => {
})

ipcMain.handle('loadProjectAtStartup', async () => {
// If we are in development mode, we don't want to load a project at
// startup.
// Since the args passed are always '.'
if (NODE_ENV !== 'production') {
return null
}

let projectPath: string | null = null
// macOS: open-file events that were received before the app is ready
const macOpenFiles: string[] = (global as any).macOpenFiles
Expand Down Expand Up @@ -206,10 +213,15 @@ ipcMain.handle('loadProjectAtStartup', async () => {
if (projectPath) {
// We have a project path, load the project information.
console.log(`Loading project at startup: ${projectPath}`)
const state = await getProjectState(projectPath)
//console.log('Project state:', JSON.stringify(state))
try {
const currentFile = await getCurrentProjectFile(projectPath)
console.log(`Project loaded: ${currentFile}`)
return currentFile
} catch (e) {
console.error(e)
}

return state
return null
}

return null
Expand Down
3 changes: 1 addition & 2 deletions src/preload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import os from 'node:os'
import fsSync from 'node:fs'
import packageJson from '../package.json'
import { MachinesListing } from 'lib/machineManager'
import { ProjectState } from 'wasm-lib/kcl/bindings/ProjectState'

const open = (args: any) => ipcRenderer.invoke('dialog.showOpenDialog', args)
const save = (args: any) => ipcRenderer.invoke('dialog.showSaveDialog', args)
Expand Down Expand Up @@ -61,7 +60,7 @@ const listMachines = async (): Promise<MachinesListing> => {
const getMachineApiIp = async (): Promise<String | null> =>
ipcRenderer.invoke('find_machine_api')

const loadProjectAtStartup = async (): Promise<ProjectState | null> =>
const loadProjectAtStartup = async (): Promise<string | null> =>
ipcRenderer.invoke('loadProjectAtStartup')

contextBridge.exposeInMainWorld('electron', {
Expand Down

0 comments on commit 6c239d2

Please sign in to comment.