Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions configs/webpack/webpack.config.renderer.dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@ const configuration: webpack.Configuration = {
extensions: ['.ts', '.js'],
alias: {
'@src': srcPath,
react: resolve(webpackPaths.rootPath, 'node_modules/react'),
'react-dom': resolve(webpackPaths.rootPath, 'node_modules/react-dom'),
},
},

Expand Down
9 changes: 8 additions & 1 deletion configs/webpack/webpack.config.renderer.prod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import CssMinimizerPlugin from 'css-minimizer-webpack-plugin'
import HtmlWebpackPlugin from 'html-webpack-plugin'
import MiniCssExtractPlugin from 'mini-css-extract-plugin'
import MonacoEditorWebpackPlugin from 'monaco-editor-webpack-plugin'
import { join } from 'path'
import { join, resolve } from 'path'
import tailwindcss from 'tailwindcss'
import TerserPlugin from 'terser-webpack-plugin'
import webpack from 'webpack'
Expand Down Expand Up @@ -117,6 +117,13 @@ const configuration: webpack.Configuration = {
],
},

resolve: {
alias: {
react: resolve(webpackPaths.rootPath, 'node_modules/react'),
'react-dom': resolve(webpackPaths.rootPath, 'node_modules/react-dom'),
},
},

optimization: {
minimize: true,
minimizer: [new TerserPlugin(), new CssMinimizerPlugin()],
Expand Down
18 changes: 15 additions & 3 deletions src/backend/editor/compiler/compiler-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2035,9 +2035,21 @@ class CompilerModule {
boolean | undefined,
]

const boardRuntime = await this.#getBoardRuntime(boardTarget) // Get the board runtime from the hals.json file

const halsContent = await CompilerModule.readJSONFile<HalsFile>(this.halsFilePath)
let boardRuntime: string
let halsContent: HalsFile
try {
_mainProcessPort.postMessage({ logLevel: 'info', message: `Resolving board target: ${boardTarget}` })
boardRuntime = await this.#getBoardRuntime(boardTarget) // Get the board runtime from the hals.json file
halsContent = await CompilerModule.readJSONFile<HalsFile>(this.halsFilePath)
} catch (error) {
_mainProcessPort.postMessage({
logLevel: 'error',
message: `Error resolving board target "${boardTarget}": ${getErrorMessage(error)}\nStopping compilation process.`,
})
_mainProcessPort.postMessage({ closePort: true })
_mainProcessPort.close()
return
}

const normalizedProjectPath = projectPath.replace('project.json', '')

Expand Down
8 changes: 4 additions & 4 deletions src/frontend/assets/icons/project/fbd/Block.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -38,17 +38,17 @@ export default function BlockIcon(props: IBlockIconProps) {
<path
d='M10.7273 9.25C10.7273 9.11193 10.8392 9 10.9773 9H17.9783C18.1164 9 18.2283 9.11193 18.2283 9.25V11.4545H10.7273V9.25Z'
fill='#B4D0FE'
fill-opacity='0.5'
fillOpacity='0.5'
/>
<line x1='5' y1='13.2955' x2='9.90909' y2='13.2955' stroke='#B4D0FE' stroke-opacity='0.5' strokeWidth='1.22727' />
<line x1='5' y1='16.5682' x2='9.90909' y2='16.5682' stroke='#B4D0FE' stroke-opacity='0.5' strokeWidth='1.22727' />
<line x1='5' y1='13.2955' x2='9.90909' y2='13.2955' stroke='#B4D0FE' strokeOpacity='0.5' strokeWidth='1.22727' />
<line x1='5' y1='16.5682' x2='9.90909' y2='16.5682' stroke='#B4D0FE' strokeOpacity='0.5' strokeWidth='1.22727' />
<line
x1='18.9091'
y1='14.9318'
x2='23.8182'
y2='14.9318'
stroke='#B4D0FE'
stroke-opacity='0.5'
strokeOpacity='0.5'
strokeWidth='1.22727'
/>
</svg>
Expand Down
10 changes: 1 addition & 9 deletions src/frontend/assets/icons/project/fbd/VariableIn.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,7 @@ export default function VariableInIcon(props: IBlockIconProps) {
stroke='#B4D0FE'
strokeWidth='1.22727'
/>
<line
x1='18'
y1='13.3864'
x2='22.9091'
y2='13.3864'
stroke='#B4D0FE'
stroke-opacity='0.5'
strokeWidth='1.22727'
/>
<line x1='18' y1='13.3864' x2='22.9091' y2='13.3864' stroke='#B4D0FE' strokeOpacity='0.5' strokeWidth='1.22727' />
</svg>
)
}
12 changes: 2 additions & 10 deletions src/frontend/assets/icons/project/fbd/VariableInOut.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,8 @@ export default function VariableInOutIcon(props: IBlockIconProps) {
stroke='#B4D0FE'
strokeWidth='1.22727'
/>
<line
x1='18'
y1='13.3864'
x2='22.9091'
y2='13.3864'
stroke='#B4D0FE'
stroke-opacity='0.5'
strokeWidth='1.22727'
/>
<line x1='4' y1='13.3864' x2='8.90909' y2='13.3864' stroke='#B4D0FE' stroke-opacity='0.5' strokeWidth='1.22727' />
<line x1='18' y1='13.3864' x2='22.9091' y2='13.3864' stroke='#B4D0FE' strokeOpacity='0.5' strokeWidth='1.22727' />
<line x1='4' y1='13.3864' x2='8.90909' y2='13.3864' stroke='#B4D0FE' strokeOpacity='0.5' strokeWidth='1.22727' />
</svg>
)
}
2 changes: 1 addition & 1 deletion src/frontend/assets/icons/project/fbd/VariableOut.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export default function VariableOutIcon(props: IBlockIconProps) {
stroke='#B4D0FE'
strokeWidth='1.22727'
/>
<line x1='4' y1='13.3864' x2='8.90909' y2='13.3864' stroke='#B4D0FE' stroke-opacity='0.5' strokeWidth='1.22727' />
<line x1='4' y1='13.3864' x2='8.90909' y2='13.3864' stroke='#B4D0FE' strokeOpacity='0.5' strokeWidth='1.22727' />
</svg>
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { UnsavedChangesWarningModal } from './unsaved-changes-warning-modal'

type BranchStatusBarProps = {
projectId: string
onBranchSwitch?: (branchName: string) => void
onBranchSwitch?: (branchName: string) => void | Promise<void>
}

export function BranchStatusBar({ projectId, onBranchSwitch }: BranchStatusBarProps) {
Expand All @@ -30,7 +30,7 @@ export function BranchStatusBar({ projectId, onBranchSwitch }: BranchStatusBarPr
try {
await versionControl.switchBranch(projectId, branch.name)
setActiveBranch(branch.name)
onBranchSwitch?.(branch.name)
void onBranchSwitch?.(branch.name)
} catch (error) {
console.error('Failed to switch branch:', error)
}
Expand All @@ -56,7 +56,7 @@ export function BranchStatusBar({ projectId, onBranchSwitch }: BranchStatusBarPr
// If we can't check, proceed with switch
}

doSwitch(branch)
void doSwitch(branch)
},
[activeBranchName, projectId, versionControl, doSwitch],
)
Expand Down Expand Up @@ -103,7 +103,7 @@ export function BranchStatusBar({ projectId, onBranchSwitch }: BranchStatusBarPr
.then(({ branches }) => {
const defaultBranch = branches.find((b) => b.isDefault)
if (defaultBranch) {
doSwitch(defaultBranch)
void doSwitch(defaultBranch)
}
})
.catch(() => {})
Expand Down Expand Up @@ -133,7 +133,7 @@ export function BranchStatusBar({ projectId, onBranchSwitch }: BranchStatusBarPr
currentBranchName={activeBranchName}
anchorRef={branchButtonRef}
onClose={() => setShowSwitcher(false)}
onSelect={handleSelect}
onSelect={(branch) => void handleSelect(branch)}
onDelete={handleDelete}
onMerge={handleMerge}
/>
Expand All @@ -152,7 +152,7 @@ export function BranchStatusBar({ projectId, onBranchSwitch }: BranchStatusBarPr
<UnsavedChangesWarningModal
isOpen={showUnsavedWarning}
targetBranchName={pendingBranchSwitch?.name ?? ''}
onDiscard={handleDiscardAndSwitch}
onDiscard={() => void handleDiscardAndSwitch()}
onCancel={handleCancelSwitch}
/>
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ export const DefaultWorkspaceActivityBar = ({ zoom }: DefaultWorkspaceActivityBa
const freshProjectData = useOpenPLCStore.getState().project.data

try {
let streamedError = false
const result = await compiler.compileProgram(
{
projectData: freshProjectData,
Expand All @@ -171,6 +172,9 @@ export const DefaultWorkspaceActivityBar = ({ zoom }: DefaultWorkspaceActivityBa
.getState()
.deviceActions.setPlcRuntimeStatus(event.plcStatus as NonNullable<RuntimeConnection['plcStatus']>)
}
if (event.level === 'error' || event.stage === 'error') {
streamedError = true
}
logCompilerEvent(event, addLog)
if (event.firmwarePath && isSimulatorBoard) {
void simulator.loadFirmware(event.firmwarePath).then((loadResult) => {
Expand All @@ -194,7 +198,7 @@ export const DefaultWorkspaceActivityBar = ({ zoom }: DefaultWorkspaceActivityBa
},
)

if (!result.success) {
if (!result.success && !streamedError) {
addLog({ id: crypto.randomUUID(), level: 'error', message: result.error ?? 'Compilation failed' })
}
} catch (err: unknown) {
Expand Down
2 changes: 1 addition & 1 deletion src/frontend/screens/workspace-screen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -469,7 +469,7 @@ const WorkspaceScreen = () => {
<ResizablePanel
id='workspacePanel'
order={2}
defaultSize={68}
defaultSize={100 - leftPanelSize}
minSize={50}
className='flex h-full min-h-0 overflow-hidden'
>
Expand Down
9 changes: 8 additions & 1 deletion src/main/modules/ipc/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1009,7 +1009,14 @@ class MainProcessBridge implements MainIpcModule {

handleRunCompileProgram = (event: IpcMainEvent, args: Array<string | PLCProjectData>) => {
const mainProcessPort = event.ports[0]
void this.compilerModule.compileProgram(args, mainProcessPort, this)
void this.compilerModule.compileProgram(args, mainProcessPort, this).catch((error) => {
mainProcessPort.postMessage({
logLevel: 'error',
message: `${getErrorMessage(error)}\nStopping compilation process.`,
})
mainProcessPort.postMessage({ closePort: true })
mainProcessPort.close()
})
}

handleRunDebugCompilation = (event: IpcMainEvent, args: Array<string | PLCProjectData>) => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,3 +128,34 @@ describe('onOpenRecent', () => {
expect(cb).not.toHaveBeenCalled()
})
})

describe('missing bridge listeners', () => {
it('returns no-op unsubscribers when the preload bridge is unavailable', () => {
window.bridge = undefined as unknown as typeof window.bridge
adapter = createEditorAcceleratorAdapter()

const methods: Array<keyof AcceleratorPort> = [
'onCreateProject',
'onOpenProject',
'onOpenRecent',
'onSaveProject',
'onSaveFile',
'onCloseProject',
'onExportProject',
'onCloseTab',
'onDeleteFile',
'onFindInProject',
'onUndo',
'onRedo',
'onSwitchPerspective',
'onAbout',
'onQuitApp',
]

for (const method of methods) {
const unsub = (adapter[method] as (cb: () => void) => () => void)(jest.fn())
expect(typeof unsub).toBe('function')
expect(unsub).not.toThrow()
}
})
})
23 changes: 23 additions & 0 deletions src/middleware/adapters/editor/__tests__/compiler-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,29 @@ describe('createEditorCompilerAdapter', () => {

expect(result).toEqual({ success: false, error: 'Compilation failed: missing file' })
expect(progressEvents.some((e) => e.stage === 'error')).toBe(true)
expect(progressEvents.some((e) => e.stage === 'done' && e.message === 'Compilation complete')).toBe(false)
})

it('ignores duplicate closePort events after an error', async () => {
const progressEvents: CompileProgressEvent[] = []
const promise = adapter.compileProgram(
{
projectData: mockProjectData,
boardTarget: 'Arduino Mega',
projectPath: '/path',
},
(event) => progressEvents.push(event),
)

await flushMicrotasks()
compileCallback!({ message: 'Board not found', logLevel: 'error' })
compileCallback!({ closePort: true })
compileCallback!({ closePort: true })

const result = await promise

expect(result).toEqual({ success: false, error: 'Board not found' })
expect(progressEvents).toEqual([{ stage: 'error', message: 'Board not found', level: 'error' }])
})

it('captures simulatorFirmwarePath as hexPath', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,12 @@ describe('createEditorDeviceAdapter', () => {
await adapter.getPreviewImage('motor-shield.png', '/path/to/pkg')
expect(window.bridge.getPreviewImage).toHaveBeenCalledWith('motor-shield.png', '/path/to/pkg')
})

it('returns empty communication ports when the preload bridge is unavailable', async () => {
window.bridge = undefined as unknown as typeof window.bridge
adapter = createEditorDeviceAdapter()

await expect(adapter.getCommunicationPorts()).resolves.toEqual([])
await expect(adapter.refreshCommunicationPorts()).resolves.toEqual([])
})
})
20 changes: 20 additions & 0 deletions src/middleware/adapters/editor/__tests__/library-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,12 @@ describe('loadAll', () => {
expect(window.bridge.loadAllLibraries).toHaveBeenCalledTimes(1)
expect(result).toEqual([{ manifest: { name: 'IEC' } }])
})

it('returns an empty list when bridge is unavailable', async () => {
window.bridge = undefined as unknown as typeof window.bridge

await expect(createEditorLibraryAdapter().loadAll()).resolves.toEqual([])
})
Comment on lines +26 to +30

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add missing fallback-path tests for installFromFile and uninstall.

This PR adds fallback branches in the adapter for missing bridge methods, but this test file does not cover those two new branches yet.

Suggested test additions
 describe('installFromFile', () => {
   it('delegates to bridge', async () => {
@@
     expect(result).toEqual({ success: true, installed: { name: 'oscat' } })
   })
+
+  it('returns a failure result when bridge is unavailable', async () => {
+    window.bridge = undefined as unknown as typeof window.bridge
+
+    await expect(createEditorLibraryAdapter().installFromFile()).resolves.toEqual({
+      success: false,
+      error: 'Library installation is not available in this context',
+    })
+  })
 })
@@
 describe('uninstall', () => {
@@
   it('falls back to a generic message when the bridge omits the error string', async () => {
@@
     expect(result).toEqual({ success: false, error: 'Uninstall failed' })
   })
+
+  it('returns a failure result when bridge is unavailable', async () => {
+    window.bridge = undefined as unknown as typeof window.bridge
+
+    await expect(createEditorLibraryAdapter().uninstall('oscat')).resolves.toEqual({
+      success: false,
+      error: 'Library uninstall is not available in this context',
+    })
+  })
 })

As per coding guidelines, "{src/frontend/store/slices/**/*.ts,src/frontend/utils/**/*.ts,src/backend/shared/**/*.ts,src/middleware/adapters/editor/**/*.ts}: Maintain 100% code coverage for: ... src/middleware/adapters/editor/".

Also applies to: 41-45, 97-103

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/middleware/adapters/editor/__tests__/library-adapter.test.ts` around
lines 26 - 30, Add two tests in library-adapter.test.ts that simulate the bridge
being unavailable by assigning window.bridge = undefined as unknown as typeof
window.bridge, then call createEditorLibraryAdapter().installFromFile(...) and
createEditorLibraryAdapter().uninstall(...), and assert each call resolves to
the adapter's fallback return (use the same fallback value used in the adapter,
e.g., false or an empty result) so the new fallback branches in installFromFile
and uninstall are covered.

})

describe('listInstalled', () => {
Expand All @@ -31,6 +37,12 @@ describe('listInstalled', () => {
expect(window.bridge.listInstalledLibraries).toHaveBeenCalledTimes(1)
expect(result).toEqual([{ name: 'IEC', bundled: true }])
})

it('returns an empty list when bridge is unavailable', async () => {
window.bridge = undefined as unknown as typeof window.bridge

await expect(createEditorLibraryAdapter().listInstalled()).resolves.toEqual([])
})
})

describe('installFromFile', () => {
Expand Down Expand Up @@ -81,4 +93,12 @@ describe('onLibrariesChanged', () => {
expect(window.bridge.onLibrariesChanged).toHaveBeenCalledWith(callback)
expect(returned).toBe(unsub)
})

it('returns a no-op unsubscribe when bridge is unavailable', () => {
window.bridge = undefined as unknown as typeof window.bridge

const returned = createEditorLibraryAdapter().onLibrariesChanged(jest.fn())

expect(() => returned()).not.toThrow()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -470,6 +470,14 @@ describe('createEditorProjectAdapter', () => {
expect(window.bridge.retrieveRecent).toHaveBeenCalledTimes(1)
expect(result).toEqual(mockRecentProjects)
})

it('returns an empty list when the preload bridge is unavailable', async () => {
window.bridge = undefined as unknown as typeof window.bridge

const result = await adapter.getRecentProjects()

expect(result).toEqual([])
})
})

describe('readFileContent', () => {
Expand Down
15 changes: 15 additions & 0 deletions src/middleware/adapters/editor/__tests__/simulator-adapter.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,21 @@ describe('onStopped', () => {
expect(window.bridge.onSimulatorStopped).toHaveBeenCalled()
})

it('does not throw when simulator stopped event bridge is unavailable', () => {
window.bridge = {
...window.bridge,
onSimulatorStopped: undefined,
} as unknown as typeof window.bridge

expect(() => createEditorSimulatorAdapter()).not.toThrow()
})

it('does not throw when the bridge is unavailable during adapter creation', () => {
window.bridge = undefined as unknown as typeof window.bridge

expect(() => createEditorSimulatorAdapter()).not.toThrow()
})

it('fires callback when main process signals stopped', async () => {
const cb = jest.fn()
adapter.onStopped(cb)
Expand Down
Loading