Compare commits
5 Commits
jtran/fix-
...
adjust-seg
Author | SHA1 | Date | |
---|---|---|---|
6c570036b4 | |||
130ecf1f88 | |||
550d8b3753 | |||
696222a070 | |||
edb424988d |
10
.github/workflows/e2e-tests.yml
vendored
10
.github/workflows/e2e-tests.yml
vendored
@ -234,10 +234,16 @@ jobs:
|
||||
shardTotal: 8
|
||||
- os: namespace-profile-macos-8-cores
|
||||
shardIndex: 1
|
||||
shardTotal: 1
|
||||
shardTotal: 2
|
||||
- os: namespace-profile-macos-8-cores
|
||||
shardIndex: 2
|
||||
shardTotal: 2
|
||||
- os: windows-latest-8-cores
|
||||
shardIndex: 1
|
||||
shardTotal: 1
|
||||
shardTotal: 2
|
||||
- os: windows-latest-8-cores
|
||||
shardIndex: 2
|
||||
shardTotal: 2
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
@ -155,6 +155,12 @@ export class CmdBarFixture {
|
||||
}
|
||||
}
|
||||
|
||||
closeCmdBar = async () => {
|
||||
const cmdBarCloseBtn = this.page.getByTestId('command-bar-close-button')
|
||||
await cmdBarCloseBtn.click()
|
||||
await expect(this.cmdBarElement).not.toBeVisible()
|
||||
}
|
||||
|
||||
get cmdSearchInput() {
|
||||
return this.page.getByTestId('cmd-bar-search')
|
||||
}
|
||||
@ -298,4 +304,27 @@ export class CmdBarFixture {
|
||||
`Monitoring text-to-cad API requests. Output will be saved to: ${outputPath}`
|
||||
)
|
||||
}
|
||||
|
||||
async toBeOpened() {
|
||||
// Check that the command bar is opened
|
||||
await expect(this.cmdBarElement).toBeVisible({ timeout: 10_000 })
|
||||
}
|
||||
|
||||
async expectArgValue(value: string) {
|
||||
// Check the placeholder project name exists
|
||||
const actualArgument = await this.cmdBarElement
|
||||
.getByTestId('cmd-bar-arg-value')
|
||||
.inputValue()
|
||||
const expectedArgument = value
|
||||
expect(actualArgument).toBe(expectedArgument)
|
||||
}
|
||||
|
||||
async expectCommandName(value: string) {
|
||||
// Check the placeholder project name exists
|
||||
const actual = await this.cmdBarElement
|
||||
.getByTestId('command-name')
|
||||
.textContent()
|
||||
const expected = value
|
||||
expect(actual).toBe(expected)
|
||||
}
|
||||
}
|
||||
|
@ -24,6 +24,7 @@ export class HomePageFixture {
|
||||
projectTextName!: Locator
|
||||
sortByDateBtn!: Locator
|
||||
sortByNameBtn!: Locator
|
||||
appHeader!: Locator
|
||||
tutorialBtn!: Locator
|
||||
|
||||
constructor(page: Page) {
|
||||
@ -44,6 +45,7 @@ export class HomePageFixture {
|
||||
|
||||
this.sortByDateBtn = this.page.getByTestId('home-sort-by-modified')
|
||||
this.sortByNameBtn = this.page.getByTestId('home-sort-by-name')
|
||||
this.appHeader = this.page.getByTestId('app-header')
|
||||
this.tutorialBtn = this.page.getByTestId('home-tutorial-button')
|
||||
}
|
||||
|
||||
@ -125,4 +127,11 @@ export class HomePageFixture {
|
||||
|
||||
await this.createAndGoToProject(name)
|
||||
}
|
||||
|
||||
isNativeFileMenuCreated = async () => {
|
||||
await expect(this.appHeader).toHaveAttribute(
|
||||
'data-native-file-menu',
|
||||
'true'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -46,6 +46,7 @@ export class SceneFixture {
|
||||
public networkToggleConnected!: Locator
|
||||
public engineConnectionsSpinner!: Locator
|
||||
public startEditSketchBtn!: Locator
|
||||
public appHeader!: Locator
|
||||
|
||||
constructor(page: Page) {
|
||||
this.page = page
|
||||
@ -57,6 +58,7 @@ export class SceneFixture {
|
||||
this.startEditSketchBtn = page
|
||||
.getByRole('button', { name: 'Start Sketch' })
|
||||
.or(page.getByRole('button', { name: 'Edit Sketch' }))
|
||||
this.appHeader = this.page.getByTestId('app-header')
|
||||
}
|
||||
private _serialiseScene = async (): Promise<SceneSerialised> => {
|
||||
const camera = await this.getCameraInfo()
|
||||
@ -280,6 +282,13 @@ export class SceneFixture {
|
||||
await expect(buttonToTest).toBeVisible()
|
||||
await buttonToTest.click()
|
||||
}
|
||||
|
||||
isNativeFileMenuCreated = async () => {
|
||||
await expect(this.appHeader).toHaveAttribute(
|
||||
'data-native-file-menu',
|
||||
'true'
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function isColourArray(
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -3496,6 +3496,73 @@ profile001 = startProfile(sketch001, at = [-102.72, 237.44])
|
||||
).toBeVisible()
|
||||
})
|
||||
|
||||
// Ensure feature tree is not showing previous file's content when switching to a file with KCL errors.
|
||||
test('Feature tree shows correct sketch count per file', async ({
|
||||
context,
|
||||
homePage,
|
||||
scene,
|
||||
toolbar,
|
||||
cmdBar,
|
||||
page,
|
||||
}) => {
|
||||
const u = await getUtils(page)
|
||||
|
||||
// Setup project with files.
|
||||
const GOOD_KCL = `sketch001 = startSketchOn(XZ)
|
||||
profile001 = startProfile(sketch001, at = [220.81, 253.8])
|
||||
|> line(end = [132.84, -151.31])
|
||||
|> line(end = [25.51, 167.15])
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()
|
||||
sketch002 = startSketchOn(XZ)
|
||||
profile002 = startProfile(sketch002, at = [158.35, -70.82])
|
||||
|> line(end = [73.9, -152.19])
|
||||
|> line(end = [85.33, 135.48])
|
||||
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
||||
|> close()`
|
||||
|
||||
const ERROR_KCL = `sketch001 = startSketchOn(XZ)
|
||||
profile001 = startProfile(sketch001, at = [127.56, 179.02])
|
||||
|> line(end = [132.84, -112.6])
|
||||
|> line(end = [85.33, 234.01])
|
||||
|> line(enfd = [-137.23, -54.55])`
|
||||
|
||||
await context.folderSetupFn(async (dir) => {
|
||||
const projectDir = path.join(dir, 'multi-file-sketch-test')
|
||||
await fs.mkdir(projectDir, { recursive: true })
|
||||
await Promise.all([
|
||||
fs.writeFile(path.join(projectDir, 'good.kcl'), GOOD_KCL, 'utf-8'),
|
||||
fs.writeFile(path.join(projectDir, 'error.kcl'), ERROR_KCL, 'utf-8'),
|
||||
])
|
||||
})
|
||||
|
||||
await page.setBodyDimensions({ width: 1000, height: 800 })
|
||||
|
||||
await homePage.openProject('multi-file-sketch-test')
|
||||
await scene.connectionEstablished()
|
||||
await scene.settled(cmdBar)
|
||||
|
||||
await u.closeDebugPanel()
|
||||
|
||||
await toolbar.openFeatureTreePane()
|
||||
await toolbar.openPane('files')
|
||||
|
||||
await toolbar.openFile('good.kcl')
|
||||
|
||||
await expect(
|
||||
toolbar.featureTreePane.getByRole('button', { name: 'Sketch' })
|
||||
).toHaveCount(2)
|
||||
|
||||
await toolbar.openFile('error.kcl')
|
||||
|
||||
// Ensure filetree is populated
|
||||
await scene.settled(cmdBar)
|
||||
|
||||
await expect(
|
||||
toolbar.featureTreePane.getByRole('button', { name: 'Sketch' })
|
||||
).toHaveCount(0)
|
||||
})
|
||||
|
||||
test('adding a syntax error, recovers after fixing', async ({
|
||||
page,
|
||||
homePage,
|
||||
|
@ -21,6 +21,7 @@ export const token = process.env.token || ''
|
||||
|
||||
import type { ProjectConfiguration } from '@rust/kcl-lib/bindings/ProjectConfiguration'
|
||||
|
||||
import type { ElectronZoo } from '@e2e/playwright/fixtures/fixtureSetup'
|
||||
import { isErrorWhitelisted } from '@e2e/playwright/lib/console-error-whitelist'
|
||||
import { TEST_SETTINGS, TEST_SETTINGS_KEY } from '@e2e/playwright/storageStates'
|
||||
import { test } from '@e2e/playwright/zoo-test'
|
||||
@ -1149,3 +1150,77 @@ export function perProjectSettingsToToml(
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
return TOML.stringify(settings as any)
|
||||
}
|
||||
|
||||
export async function clickElectronNativeMenuById(
|
||||
tronApp: ElectronZoo,
|
||||
menuId: string
|
||||
) {
|
||||
const clickWasTriggered = await tronApp.electron.evaluate(
|
||||
async ({ app }, menuId) => {
|
||||
if (!app || !app.applicationMenu) {
|
||||
return false
|
||||
}
|
||||
const menu = app.applicationMenu.getMenuItemById(menuId)
|
||||
if (!menu) return false
|
||||
menu.click()
|
||||
return true
|
||||
},
|
||||
menuId
|
||||
)
|
||||
expect(clickWasTriggered).toBe(true)
|
||||
}
|
||||
|
||||
export async function findElectronNativeMenuById(
|
||||
tronApp: ElectronZoo,
|
||||
menuId: string
|
||||
) {
|
||||
const found = await tronApp.electron.evaluate(async ({ app }, menuId) => {
|
||||
if (!app || !app.applicationMenu) {
|
||||
return false
|
||||
}
|
||||
const menu = app.applicationMenu.getMenuItemById(menuId)
|
||||
if (!menu) return false
|
||||
return true
|
||||
}, menuId)
|
||||
expect(found).toBe(true)
|
||||
}
|
||||
|
||||
export async function openSettingsExpectText(page: Page, text: string) {
|
||||
const settings = page.getByTestId('settings-dialog-panel')
|
||||
await expect(settings).toBeVisible()
|
||||
// You are viewing the user tab
|
||||
const actualText = settings.getByText(text)
|
||||
await expect(actualText).toBeVisible()
|
||||
}
|
||||
|
||||
export async function openSettingsExpectLocator(page: Page, selector: string) {
|
||||
const settings = page.getByTestId('settings-dialog-panel')
|
||||
await expect(settings).toBeVisible()
|
||||
// You are viewing the keybindings tab
|
||||
const settingsLocator = settings.locator(selector)
|
||||
await expect(settingsLocator).toBeVisible()
|
||||
}
|
||||
|
||||
/**
|
||||
* A developer helper function to make playwright send all the console logs to stdout
|
||||
* Call this within your E2E test and pass in the page or the tronApp to get as many
|
||||
* logs piped to stdout for debugging
|
||||
*/
|
||||
export async function enableConsoleLogEverything({
|
||||
page,
|
||||
tronApp,
|
||||
}: { page?: Page; tronApp?: ElectronZoo }) {
|
||||
page?.on('console', (msg) => {
|
||||
console.log(`[Page-log]: ${msg.text()}`)
|
||||
})
|
||||
|
||||
tronApp?.electron.on('window', async (electronPage) => {
|
||||
electronPage.on('console', (msg) => {
|
||||
console.log(`[Renderer] ${msg.type()}: ${msg.text()}`)
|
||||
})
|
||||
})
|
||||
|
||||
tronApp?.electron.on('console', (msg) => {
|
||||
console.log(`[Main] ${msg.type()}: ${msg.text()}`)
|
||||
})
|
||||
}
|
||||
|
@ -237,7 +237,7 @@ test.describe('Testing segment overlays', () => {
|
||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
await expect(page.getByTestId('segment-overlay')).toHaveCount(13)
|
||||
await expect(page.getByTestId('segment-overlay')).toHaveCount(14)
|
||||
|
||||
const clickUnconstrained = _clickUnconstrained(page, editor)
|
||||
const clickConstrained = _clickConstrained(page, editor)
|
||||
@ -402,7 +402,7 @@ test.describe('Testing segment overlays', () => {
|
||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
await expect(page.getByTestId('segment-overlay')).toHaveCount(8)
|
||||
await expect(page.getByTestId('segment-overlay')).toHaveCount(9)
|
||||
|
||||
const clickUnconstrained = _clickUnconstrained(page, editor)
|
||||
|
||||
@ -482,7 +482,7 @@ test.describe('Testing segment overlays', () => {
|
||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
await expect(page.getByTestId('segment-overlay')).toHaveCount(13)
|
||||
await expect(page.getByTestId('segment-overlay')).toHaveCount(14)
|
||||
|
||||
const clickUnconstrained = _clickUnconstrained(page, editor)
|
||||
const clickConstrained = _clickConstrained(page, editor)
|
||||
@ -602,7 +602,7 @@ test.describe('Testing segment overlays', () => {
|
||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
await expect(page.getByTestId('segment-overlay')).toHaveCount(13)
|
||||
await expect(page.getByTestId('segment-overlay')).toHaveCount(14)
|
||||
|
||||
const clickUnconstrained = _clickUnconstrained(page, editor)
|
||||
const clickConstrained = _clickConstrained(page, editor)
|
||||
@ -808,7 +808,7 @@ profile001 = startProfile(sketch001, at = [56.37, 120.33])
|
||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||
await page.waitForTimeout(500)
|
||||
|
||||
await expect(page.getByTestId('segment-overlay')).toHaveCount(5)
|
||||
await expect(page.getByTestId('segment-overlay')).toHaveCount(6)
|
||||
|
||||
const clickUnconstrained = _clickUnconstrained(page, editor)
|
||||
const clickConstrained = _clickConstrained(page, editor)
|
||||
@ -1307,7 +1307,7 @@ part001 = startSketchOn(XZ)
|
||||
.toBe(true)
|
||||
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||
|
||||
await expect(page.getByTestId('segment-overlay')).toHaveCount(3)
|
||||
await expect(page.getByTestId('segment-overlay')).toHaveCount(4)
|
||||
const segmentToDelete = await u.getBoundingBox(
|
||||
`[data-overlay-index="0"]`
|
||||
)
|
||||
|
@ -1249,7 +1249,7 @@ secondSketch = startSketchOn(part001, face = '')
|
||||
let err = err.as_kcl_error().unwrap();
|
||||
assert_eq!(
|
||||
err.message(),
|
||||
"The arg face was given, but it was the wrong type. It should be type FaceTag but it was string (text)"
|
||||
"The arg face was given, but it was the wrong type. It should be type FaceTag but it was string"
|
||||
);
|
||||
}
|
||||
|
||||
@ -1882,7 +1882,7 @@ someFunction('INVALID')
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.err().unwrap().to_string(),
|
||||
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([46, 55, 0]), SourceRange([60, 83, 0])], message: "This function expected the input argument to be Solid or Plane but it's actually of type string (text)" }"#
|
||||
r#"semantic: KclErrorDetails { source_ranges: [SourceRange([46, 55, 0]), SourceRange([60, 83, 0])], message: "This function expected the input argument to be Solid or Plane but it's actually of type string" }"#
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -913,11 +913,9 @@ impl Node<MemberExpression> {
|
||||
}),
|
||||
(being_indexed, _, _) => {
|
||||
let t = being_indexed.human_friendly_type();
|
||||
let article = article_for(t);
|
||||
let article = article_for(&t);
|
||||
Err(KclError::Semantic(KclErrorDetails {
|
||||
message: format!(
|
||||
"Only arrays and objects can be indexed, but you're trying to index {article} {t}"
|
||||
),
|
||||
message: format!("Only arrays can be indexed, but you're trying to index {article} {t}"),
|
||||
source_ranges: vec![self.clone().into()],
|
||||
}))
|
||||
}
|
||||
@ -1698,8 +1696,9 @@ impl Node<ObjectExpression> {
|
||||
}
|
||||
}
|
||||
|
||||
fn article_for(s: &str) -> &'static str {
|
||||
if s.starts_with(['a', 'e', 'i', 'o', 'u']) {
|
||||
fn article_for<S: AsRef<str>>(s: S) -> &'static str {
|
||||
// '[' is included since it's an array.
|
||||
if s.as_ref().starts_with(['a', 'e', 'i', 'o', 'u', '[']) {
|
||||
"an"
|
||||
} else {
|
||||
"a"
|
||||
@ -1709,10 +1708,9 @@ fn article_for(s: &str) -> &'static str {
|
||||
fn number_as_f64(v: &KclValue, source_range: SourceRange) -> Result<TyF64, KclError> {
|
||||
v.as_ty_f64().ok_or_else(|| {
|
||||
let actual_type = v.human_friendly_type();
|
||||
let article = article_for(actual_type);
|
||||
KclError::Semantic(KclErrorDetails {
|
||||
source_ranges: vec![source_range],
|
||||
message: format!("Expected a number, but found {article} {actual_type}",),
|
||||
message: format!("Expected a number, but found {actual_type}",),
|
||||
})
|
||||
})
|
||||
}
|
||||
@ -2446,19 +2444,23 @@ arr1 = [42]: [number(cm)]
|
||||
a = 42: string
|
||||
"#;
|
||||
let result = parse_execute(program).await;
|
||||
assert!(result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("could not coerce number value to type string"));
|
||||
let err = result.unwrap_err();
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("could not coerce number(default units) value to type string"),
|
||||
"Expected error but found {err:?}"
|
||||
);
|
||||
|
||||
let program = r#"
|
||||
a = 42: Plane
|
||||
"#;
|
||||
let result = parse_execute(program).await;
|
||||
assert!(result
|
||||
.unwrap_err()
|
||||
.to_string()
|
||||
.contains("could not coerce number value to type Plane"));
|
||||
let err = result.unwrap_err();
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("could not coerce number(default units) value to type Plane"),
|
||||
"Expected error but found {err:?}"
|
||||
);
|
||||
|
||||
let program = r#"
|
||||
arr = [0]: [string]
|
||||
@ -2467,7 +2469,7 @@ arr = [0]: [string]
|
||||
let err = result.unwrap_err();
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("could not coerce array (list) value to type [string]"),
|
||||
.contains("could not coerce [any; 1] value to type [string]"),
|
||||
"Expected error but found {err:?}"
|
||||
);
|
||||
|
||||
@ -2478,7 +2480,7 @@ mixedArr = [0, "a"]: [number(mm)]
|
||||
let err = result.unwrap_err();
|
||||
assert!(
|
||||
err.to_string()
|
||||
.contains("could not coerce array (list) value to type [number(mm)]"),
|
||||
.contains("could not coerce [any; 2] value to type [number(mm)]"),
|
||||
"Expected error but found {err:?}"
|
||||
);
|
||||
}
|
||||
|
@ -280,7 +280,10 @@ impl KclValue {
|
||||
|
||||
/// Human readable type name used in error messages. Should not be relied
|
||||
/// on for program logic.
|
||||
pub(crate) fn human_friendly_type(&self) -> &'static str {
|
||||
pub(crate) fn human_friendly_type(&self) -> String {
|
||||
if let Some(t) = self.principal_type() {
|
||||
return t.to_string();
|
||||
}
|
||||
match self {
|
||||
KclValue::Uuid { .. } => "Unique ID (uuid)",
|
||||
KclValue::TagDeclarator(_) => "TagDeclarator",
|
||||
@ -314,6 +317,7 @@ impl KclValue {
|
||||
KclValue::Type { .. } => "type",
|
||||
KclValue::KclNone { .. } => "None",
|
||||
}
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
pub(crate) fn from_literal(literal: Node<Literal>, exec_state: &mut ExecState) -> Self {
|
||||
|
@ -1910,13 +1910,13 @@ notNull = !myNull
|
||||
"#;
|
||||
assert_eq!(
|
||||
parse_execute(code1).await.unwrap_err().message(),
|
||||
"Cannot apply unary operator ! to non-boolean value: number",
|
||||
"Cannot apply unary operator ! to non-boolean value: number(default units)",
|
||||
);
|
||||
|
||||
let code2 = "notZero = !0";
|
||||
assert_eq!(
|
||||
parse_execute(code2).await.unwrap_err().message(),
|
||||
"Cannot apply unary operator ! to non-boolean value: number",
|
||||
"Cannot apply unary operator ! to non-boolean value: number(default units)",
|
||||
);
|
||||
|
||||
let code3 = r#"
|
||||
@ -1924,7 +1924,7 @@ notEmptyString = !""
|
||||
"#;
|
||||
assert_eq!(
|
||||
parse_execute(code3).await.unwrap_err().message(),
|
||||
"Cannot apply unary operator ! to non-boolean value: string (text)",
|
||||
"Cannot apply unary operator ! to non-boolean value: string",
|
||||
);
|
||||
|
||||
let code4 = r#"
|
||||
@ -1933,7 +1933,7 @@ notMember = !obj.a
|
||||
"#;
|
||||
assert_eq!(
|
||||
parse_execute(code4).await.unwrap_err().message(),
|
||||
"Cannot apply unary operator ! to non-boolean value: number",
|
||||
"Cannot apply unary operator ! to non-boolean value: number(default units)",
|
||||
);
|
||||
|
||||
let code5 = "
|
||||
@ -1941,7 +1941,7 @@ a = []
|
||||
notArray = !a";
|
||||
assert_eq!(
|
||||
parse_execute(code5).await.unwrap_err().message(),
|
||||
"Cannot apply unary operator ! to non-boolean value: array (list)",
|
||||
"Cannot apply unary operator ! to non-boolean value: [any; 0]",
|
||||
);
|
||||
|
||||
let code6 = "
|
||||
@ -1949,7 +1949,7 @@ x = {}
|
||||
notObject = !x";
|
||||
assert_eq!(
|
||||
parse_execute(code6).await.unwrap_err().message(),
|
||||
"Cannot apply unary operator ! to non-boolean value: object",
|
||||
"Cannot apply unary operator ! to non-boolean value: { }",
|
||||
);
|
||||
|
||||
let code7 = "
|
||||
@ -1975,7 +1975,7 @@ notTagDeclarator = !myTagDeclarator";
|
||||
assert!(
|
||||
tag_declarator_err
|
||||
.message()
|
||||
.starts_with("Cannot apply unary operator ! to non-boolean value: TagDeclarator"),
|
||||
.starts_with("Cannot apply unary operator ! to non-boolean value: tag"),
|
||||
"Actual error: {:?}",
|
||||
tag_declarator_err
|
||||
);
|
||||
@ -1989,7 +1989,7 @@ notTagIdentifier = !myTag";
|
||||
assert!(
|
||||
tag_identifier_err
|
||||
.message()
|
||||
.starts_with("Cannot apply unary operator ! to non-boolean value: TagIdentifier"),
|
||||
.starts_with("Cannot apply unary operator ! to non-boolean value: tag"),
|
||||
"Actual error: {:?}",
|
||||
tag_identifier_err
|
||||
);
|
||||
|
@ -257,14 +257,22 @@ impl Args {
|
||||
};
|
||||
|
||||
let arg = arg.value.coerce(ty, exec_state).map_err(|_| {
|
||||
let actual_type_name = arg.value.human_friendly_type();
|
||||
let actual_type = arg.value.principal_type();
|
||||
let actual_type_name = actual_type
|
||||
.as_ref()
|
||||
.map(|t| t.to_string())
|
||||
.unwrap_or_else(|| arg.value.human_friendly_type().to_owned());
|
||||
let msg_base = format!(
|
||||
"This function expected the input argument to be {} but it's actually of type {actual_type_name}",
|
||||
ty.human_friendly_type(),
|
||||
);
|
||||
let suggestion = match (ty, actual_type_name) {
|
||||
(RuntimeType::Primitive(PrimitiveType::Solid), "Sketch") => Some(ERROR_STRING_SKETCH_TO_SOLID_HELPER),
|
||||
(RuntimeType::Array(t, _), "Sketch") if **t == RuntimeType::Primitive(PrimitiveType::Solid) => {
|
||||
let suggestion = match (ty, actual_type) {
|
||||
(RuntimeType::Primitive(PrimitiveType::Solid), Some(RuntimeType::Primitive(PrimitiveType::Sketch))) => {
|
||||
Some(ERROR_STRING_SKETCH_TO_SOLID_HELPER)
|
||||
}
|
||||
(RuntimeType::Array(t, _), Some(RuntimeType::Primitive(PrimitiveType::Sketch)))
|
||||
if **t == RuntimeType::Primitive(PrimitiveType::Solid) =>
|
||||
{
|
||||
Some(ERROR_STRING_SKETCH_TO_SOLID_HELPER)
|
||||
}
|
||||
_ => None,
|
||||
@ -381,14 +389,22 @@ impl Args {
|
||||
}))?;
|
||||
|
||||
let arg = arg.value.coerce(ty, exec_state).map_err(|_| {
|
||||
let actual_type_name = arg.value.human_friendly_type();
|
||||
let actual_type = arg.value.principal_type();
|
||||
let actual_type_name = actual_type
|
||||
.as_ref()
|
||||
.map(|t| t.to_string())
|
||||
.unwrap_or_else(|| arg.value.human_friendly_type().to_owned());
|
||||
let msg_base = format!(
|
||||
"This function expected the input argument to be {} but it's actually of type {actual_type_name}",
|
||||
ty.human_friendly_type(),
|
||||
);
|
||||
let suggestion = match (ty, actual_type_name) {
|
||||
(RuntimeType::Primitive(PrimitiveType::Solid), "Sketch") => Some(ERROR_STRING_SKETCH_TO_SOLID_HELPER),
|
||||
(RuntimeType::Array(ty, _), "Sketch") if **ty == RuntimeType::Primitive(PrimitiveType::Solid) => {
|
||||
let suggestion = match (ty, actual_type) {
|
||||
(RuntimeType::Primitive(PrimitiveType::Solid), Some(RuntimeType::Primitive(PrimitiveType::Sketch))) => {
|
||||
Some(ERROR_STRING_SKETCH_TO_SOLID_HELPER)
|
||||
}
|
||||
(RuntimeType::Array(ty, _), Some(RuntimeType::Primitive(PrimitiveType::Sketch)))
|
||||
if **ty == RuntimeType::Primitive(PrimitiveType::Solid) =>
|
||||
{
|
||||
Some(ERROR_STRING_SKETCH_TO_SOLID_HELPER)
|
||||
}
|
||||
_ => None,
|
||||
|
@ -4,8 +4,7 @@ description: Error from executing argument_error.kcl
|
||||
---
|
||||
KCL Semantic error
|
||||
|
||||
× semantic: f requires a value with type `fn(any): any`, but found array
|
||||
│ (list)
|
||||
× semantic: f requires a value with type `fn(any): any`, but found [any; 2]
|
||||
╭─[5:1]
|
||||
4 │
|
||||
5 │ map(f, f = [0, 1])
|
||||
@ -16,7 +15,7 @@ KCL Semantic error
|
||||
╰─▶ KCL Semantic error
|
||||
|
||||
× semantic: f requires a value with type `fn(any): any`, but found
|
||||
│ array (list)
|
||||
│ [any; 2]
|
||||
╭─[5:12]
|
||||
4 │
|
||||
5 │ map(f, f = [0, 1])
|
||||
|
@ -5,7 +5,7 @@ description: Error from executing array_elem_pop_empty_fail.kcl
|
||||
KCL Semantic error
|
||||
|
||||
× semantic: The input argument of `std::array::pop` requires a value with
|
||||
│ type `[any; 1+]`, but found array (list)
|
||||
│ type `[any; 1+]`, but found [any; 0]
|
||||
╭─[2:8]
|
||||
1 │ arr = []
|
||||
2 │ fail = pop(arr)
|
||||
@ -16,7 +16,7 @@ KCL Semantic error
|
||||
╰─▶ KCL Semantic error
|
||||
|
||||
× semantic: The input argument of `std::array::pop` requires a value
|
||||
│ with type `[any; 1+]`, but found array (list)
|
||||
│ with type `[any; 1+]`, but found [any; 0]
|
||||
╭─[2:12]
|
||||
1 │ arr = []
|
||||
2 │ fail = pop(arr)
|
||||
|
@ -4,7 +4,7 @@ description: Error from executing comparisons_multiple.kcl
|
||||
---
|
||||
KCL Semantic error
|
||||
|
||||
× semantic: Expected a number, but found a boolean (true/false value)
|
||||
× semantic: Expected a number, but found bool
|
||||
╭────
|
||||
1 │ assert(3 == 3 == 3, error = "this should not compile")
|
||||
· ───┬──
|
||||
|
@ -5,7 +5,7 @@ description: Error from executing error_inside_fn_also_has_source_range_of_call_
|
||||
KCL Semantic error
|
||||
|
||||
× semantic: This function expected the input argument to be Solid or Plane
|
||||
│ but it's actually of type string (text)
|
||||
│ but it's actually of type string
|
||||
╭─[3:23]
|
||||
2 │ fn someNestedFunction(@something2) {
|
||||
3 │ startSketchOn(something2)
|
||||
@ -25,7 +25,7 @@ KCL Semantic error
|
||||
├─▶ KCL Semantic error
|
||||
│
|
||||
│ × semantic: This function expected the input argument to be Solid or
|
||||
│ │ Plane but it's actually of type string (text)
|
||||
│ │ Plane but it's actually of type string
|
||||
│ ╭─[3:23]
|
||||
│ 2 │ fn someNestedFunction(@something2) {
|
||||
│ 3 │ startSketchOn(something2)
|
||||
@ -37,7 +37,7 @@ KCL Semantic error
|
||||
╰─▶ KCL Semantic error
|
||||
|
||||
× semantic: This function expected the input argument to be Solid or
|
||||
│ Plane but it's actually of type string (text)
|
||||
│ Plane but it's actually of type string
|
||||
╭─[6:5]
|
||||
5 │
|
||||
6 │ someNestedFunction(something)
|
||||
|
@ -1,11 +1,11 @@
|
||||
---
|
||||
source: kcl/src/simulation_tests.rs
|
||||
source: kcl-lib/src/simulation_tests.rs
|
||||
description: Error from executing invalid_member_object.kcl
|
||||
---
|
||||
KCL Semantic error
|
||||
|
||||
× semantic: Only arrays and objects can be indexed, but you're trying to
|
||||
│ index a number
|
||||
× semantic: Only arrays can be indexed, but you're trying to index a
|
||||
│ number(default units)
|
||||
╭─[2:5]
|
||||
1 │ num = 999
|
||||
2 │ x = num[3]
|
||||
|
@ -5,7 +5,7 @@ description: Error from executing panic_repro_cube.kcl
|
||||
KCL Semantic error
|
||||
|
||||
× semantic: This function expected the input argument to be tag identifier
|
||||
│ but it's actually of type Unique ID (uuid)
|
||||
│ but it's actually of type tag
|
||||
╭─[43:25]
|
||||
42 │ // these double wrapped functions are the point of this test
|
||||
43 │ getNextAdjacentEdge(getNextAdjacentEdge(seg01)),
|
||||
|
17
src/App.tsx
17
src/App.tsx
@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef } from 'react'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import toast from 'react-hot-toast'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import ModalContainer from 'react-modal-promise'
|
||||
@ -42,6 +42,7 @@ import {
|
||||
ONBOARDING_TOAST_ID,
|
||||
TutorialRequestToast,
|
||||
} from '@src/routes/Onboarding/utils'
|
||||
import { reportRejection } from '@src/lib/trap'
|
||||
|
||||
// CYCLIC REF
|
||||
sceneInfra.camControls.engineStreamActor = engineStreamActor
|
||||
@ -52,6 +53,7 @@ maybeWriteToDisk()
|
||||
|
||||
export function App() {
|
||||
const { project, file } = useLoaderData() as IndexLoaderData
|
||||
const [nativeFileMenuCreated, setNativeFileMenuCreated] = useState(false)
|
||||
|
||||
// Keep a lookout for a URL query string that invokes the 'import file from URL' command
|
||||
useCreateFileLinkQuery((argDefaultValues) => {
|
||||
@ -145,12 +147,25 @@ export function App() {
|
||||
}
|
||||
}, [location, settings.app.onboardingStatus, navigate])
|
||||
|
||||
// Only create the native file menus on desktop
|
||||
useEffect(() => {
|
||||
if (isDesktop()) {
|
||||
window.electron
|
||||
.createModelingPageMenu()
|
||||
.then(() => {
|
||||
setNativeFileMenuCreated(true)
|
||||
})
|
||||
.catch(reportRejection)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<div className="relative h-full flex flex-col" ref={ref}>
|
||||
<AppHeader
|
||||
className="transition-opacity transition-duration-75"
|
||||
project={{ project, file }}
|
||||
enableMenu={true}
|
||||
nativeFileMenuCreated={nativeFileMenuCreated}
|
||||
>
|
||||
<CommandBarOpenButton />
|
||||
<ShareButton />
|
||||
|
@ -14,6 +14,7 @@ interface AppHeaderProps extends React.PropsWithChildren {
|
||||
className?: string
|
||||
enableMenu?: boolean
|
||||
style?: React.CSSProperties
|
||||
nativeFileMenuCreated: boolean
|
||||
}
|
||||
|
||||
export const AppHeader = ({
|
||||
@ -23,12 +24,14 @@ export const AppHeader = ({
|
||||
className = '',
|
||||
style,
|
||||
enableMenu = false,
|
||||
nativeFileMenuCreated,
|
||||
}: AppHeaderProps) => {
|
||||
const user = useUser()
|
||||
|
||||
return (
|
||||
<header
|
||||
id="app-header"
|
||||
data-testid="app-header"
|
||||
className={
|
||||
'w-full grid ' +
|
||||
styles.header +
|
||||
@ -37,6 +40,7 @@ export const AppHeader = ({
|
||||
}overlaid-panes sticky top-0 z-20 px-2 items-start ` +
|
||||
className
|
||||
}
|
||||
data-native-file-menu={nativeFileMenuCreated}
|
||||
style={style}
|
||||
>
|
||||
<ProjectSidebarMenu
|
||||
|
@ -169,6 +169,7 @@ export const CommandBar = () => {
|
||||
)}
|
||||
<div className="flex flex-col gap-2 !absolute left-auto right-full top-[-3px] m-2.5 p-0 border-none bg-transparent hover:bg-transparent">
|
||||
<button
|
||||
data-testid="command-bar-close-button"
|
||||
onClick={() => commandBarActor.send({ type: 'Close' })}
|
||||
className="group m-0 p-0 border-none bg-transparent hover:bg-transparent"
|
||||
>
|
||||
|
@ -29,7 +29,7 @@ import { kclCommands } from '@src/lib/kclCommands'
|
||||
import { BROWSER_PATH, PATHS } from '@src/lib/paths'
|
||||
import { markOnce } from '@src/lib/performance'
|
||||
import { codeManager, kclManager } from '@src/lib/singletons'
|
||||
import { err, reportRejection } from '@src/lib/trap'
|
||||
import { err } from '@src/lib/trap'
|
||||
import { type IndexLoaderData } from '@src/lib/types'
|
||||
import { useSettings, useToken } from '@src/lib/singletons'
|
||||
import { commandBarActor } from '@src/lib/singletons'
|
||||
@ -59,12 +59,6 @@ export const FileMachineProvider = ({
|
||||
const { project, file } = projectData
|
||||
|
||||
const filePath = useAbsoluteFilePath()
|
||||
// Only create the native file menus on desktop
|
||||
useEffect(() => {
|
||||
if (isDesktop()) {
|
||||
window.electron.createModelingPageMenu().catch(reportRejection)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
const {
|
||||
|
@ -147,6 +147,10 @@ export class KclManager {
|
||||
|
||||
set switchedFiles(switchedFiles: boolean) {
|
||||
this._switchedFiles = switchedFiles
|
||||
|
||||
// These belonged to the previous file
|
||||
this.lastSuccessfulOperations = []
|
||||
this.lastSuccessfulVariables = {}
|
||||
}
|
||||
|
||||
get variables() {
|
||||
|
@ -1,5 +1,5 @@
|
||||
import type { FormEvent, HTMLProps } from 'react'
|
||||
import { useEffect } from 'react'
|
||||
import { useEffect, useState } from 'react'
|
||||
import { toast } from 'react-hot-toast'
|
||||
import { useHotkeys } from 'react-hotkeys-hook'
|
||||
import {
|
||||
@ -70,13 +70,20 @@ type ReadWriteProjectState = {
|
||||
// This route only opens in the desktop context for now,
|
||||
// as defined in Router.tsx, so we can use the desktop APIs and types.
|
||||
const Home = () => {
|
||||
const navigate = useNavigate()
|
||||
const readWriteProjectDir = useCanReadWriteProjectDirectory()
|
||||
const [nativeFileMenuCreated, setNativeFileMenuCreated] = useState(false)
|
||||
const apiToken = useToken()
|
||||
|
||||
// Only create the native file menus on desktop
|
||||
useEffect(() => {
|
||||
if (isDesktop()) {
|
||||
window.electron.createHomePageMenu().catch(reportRejection)
|
||||
window.electron
|
||||
.createHomePageMenu()
|
||||
.then(() => {
|
||||
setNativeFileMenuCreated(true)
|
||||
})
|
||||
.catch(reportRejection)
|
||||
}
|
||||
billingActor.send({ type: BillingTransition.Update, apiToken })
|
||||
}, [])
|
||||
@ -94,7 +101,6 @@ const Home = () => {
|
||||
})
|
||||
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const settings = useSettings()
|
||||
const onboardingStatus = settings.app.onboardingStatus.current
|
||||
|
||||
@ -217,7 +223,10 @@ const Home = () => {
|
||||
|
||||
return (
|
||||
<div className="relative flex flex-col items-stretch h-screen w-screen overflow-hidden">
|
||||
<AppHeader showToolbar={false} />
|
||||
<AppHeader
|
||||
nativeFileMenuCreated={nativeFileMenuCreated}
|
||||
showToolbar={false}
|
||||
/>
|
||||
<div className="overflow-hidden self-stretch w-full flex-1 home-layout max-w-4xl lg:max-w-5xl xl:max-w-7xl mb-12 px-4 mx-auto mt-8 lg:mt-24 lg:px-0">
|
||||
<HomeHeader
|
||||
setQuery={setQuery}
|
||||
|
@ -32,13 +32,16 @@ export const Settings = () => {
|
||||
useEffect(() => {
|
||||
console.log('hash', location.hash)
|
||||
if (location.hash) {
|
||||
const element = document.getElementById(location.hash.slice(1))
|
||||
if (element) {
|
||||
element.scrollIntoView({ block: 'center', behavior: 'smooth' })
|
||||
;(
|
||||
element.querySelector('input, select, textarea') as HTMLInputElement
|
||||
)?.focus()
|
||||
}
|
||||
setTimeout(() => {
|
||||
// GOTCHA: Next tick required, you can instantly navigate to a path and this code will find a null element and not scroll into view.
|
||||
const element = document.getElementById(location.hash.slice(1))
|
||||
if (element) {
|
||||
element.scrollIntoView({ block: 'center', behavior: 'smooth' })
|
||||
;(
|
||||
element.querySelector('input, select, textarea') as HTMLInputElement
|
||||
)?.focus()
|
||||
}
|
||||
}, 0)
|
||||
}
|
||||
}, [location.hash])
|
||||
|
||||
|
Reference in New Issue
Block a user