Part of #4600. PR: https://github.com/KittyCAD/modeling-app/pull/4826 # Changes to KCL stdlib - `line(point, sketch, tag)` and `lineTo(point, sketch, tag)` are combined into `line(@sketch, end?, endAbsolute?, tag?)` - `close(sketch, tag?)` is now `close(@sketch, tag?)` - `extrude(length, sketch)` is now `extrude(@sketch, length)` Note that if a parameter starts with `@` like `@sketch`, it doesn't have any label when called, so you call it like this: ``` sketch = startSketchAt([0, 0]) line(sketch, end = [3, 3], tag = $hi) ``` Note also that if you're using a `|>` pipeline, you can omit the `@` argument and it will be assumed to be the LHS of the `|>`. So the above could be written as ``` sketch = startSketchAt([0, 0]) |> line(end = [3, 3], tag = $hi) ``` Also changes frontend tests to use KittyCAD/kcl-samples#139 instead of its main The regex find-and-replace I use for migrating code (note these don't work with multi-line expressions) are: ``` line\(([^=]*), %\) line(end = $1) line\((.*), %, (.*)\) line(end = $1, tag = $2) lineTo\((.*), %\) line(endAbsolute = $1) lineTo\((.*), %, (.*)\) line(endAbsolute = $1, tag = $2) extrude\((.*), %\) extrude(length = $1) extrude\(([^=]*), ([a-zA-Z0-9]+)\) extrude($2, length = $1) close\(%, (.*)\) close(tag = $1) ``` # Selected notes from commits before I squash them all * Fix test 'yRelative to horizontal distance' Fixes: - Make a lineTo helper - Fix pathToNode to go through the labeled arg .arg property * Fix test by changing lookups into transformMap Parts of the code assumed that `line` is always a relative call. But actually now it might be absolute, if it's got an `endAbsolute` parameter. So, change whether to look up `line` or `lineTo` and the relevant absolute or relative line types based on that parameter. * Stop asserting on exact source ranges When I changed line to kwargs, all the source ranges we assert on became slightly different. I find these assertions to be very very low value. So I'm removing them. * Fix more tests: getConstraintType calls weren't checking if the 'line' fn was absolute or relative. * Fixed another queryAst test There were 2 problems: - Test was looking for the old style of `line` call to choose an offset for pathToNode - Test assumed that the `tag` param was always the third one, but in a kwarg call, you have to look it up by label * Fix test: traverse was not handling CallExpressionKw * Fix another test, addTagKw addTag helper was not aware of kw args. * Convert close from positional to kwargs If the close() call has 0 args, or a single unlabeled arg, the parser interprets it as a CallExpression (positional) not a CallExpressionKw. But then if a codemod wants to add a tag to it, it tries adding a kwarg called 'tag', which fails because the CallExpression doesn't need kwargs inserted into it. The fix is: change the node from CallExpression to CallExpressionKw, and update getNodeFromPath to take a 'replacement' arg, so we can replace the old node with the new node in the AST. * Fix the last test Test was looking for `lineTo` as a substring of the input KCL program. But there's no more lineTo function, so I changed it to look for line() with an endAbsolute arg, which is the new equivalent. Also changed the getConstraintInfo code to look up the lineTo if using line with endAbsolute. * Fix many bad regex find-replaces I wrote a regex find-and-replace which converted `line` calls from positional to keyword calls. But it was accidentally applied to more places than it should be, for example, angledLine, xLine and yLine calls. Fixes this. * Fixes test 'Basic sketch › code pane closed at start' Problem was, the getNodeFromPath call might not actually find a callExpressionKw, it might find a callExpression. So the `giveSketchFnCallTag` thought it was modifying a kwargs call, but it was actually modifying a positional call. This meant it tried to push a labeled argument in, rather than a normal arg, and a lot of other problems. Fixed by doing runtime typechecking. * Fix: Optional args given with wrong type were silently ignored Optional args don't have to be given. But if the user gives them, they should be the right type. Bug: if the KCL interpreter found an optional arg, which was given, but was the wrong type, it would ignore it and pretend the arg was never given at all. This was confusing for users. Fix: Now if you give an optional arg, but it's the wrong type, KCL will emit a type error just like it would for a mandatory argument. --------- Signed-off-by: Nick Cameron <nrc@ncameron.org> Co-authored-by: Nick Cameron <nrc@ncameron.org> Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: Frank Noirot <frank@kittycad.io> Co-authored-by: Kevin Nadro <kevin@zoo.dev> Co-authored-by: Jonathan Tran <jonnytran@gmail.com>
900 lines
32 KiB
TypeScript
900 lines
32 KiB
TypeScript
import { test, expect } from './zoo-test'
|
|
import * as fsp from 'fs/promises'
|
|
import { join } from 'path'
|
|
import { getUtils, executorInputPath, createProject } from './test-utils'
|
|
import { SaveSettingsPayload, SettingsLevel } from 'lib/settings/settingsTypes'
|
|
import { SETTINGS_FILE_NAME, PROJECT_SETTINGS_FILE_NAME } from 'lib/constants'
|
|
import {
|
|
TEST_SETTINGS_KEY,
|
|
TEST_SETTINGS_CORRUPTED,
|
|
TEST_SETTINGS,
|
|
TEST_SETTINGS_DEFAULT_THEME,
|
|
} from './storageStates'
|
|
import * as TOML from '@iarna/toml'
|
|
|
|
test.describe('Testing settings', () => {
|
|
test(
|
|
'Stored settings are validated and fall back to defaults',
|
|
// Override beforeEach test setup
|
|
// with corrupted settings
|
|
{
|
|
appSettings: TEST_SETTINGS_CORRUPTED,
|
|
},
|
|
async ({ page, homePage }) => {
|
|
await page.setBodyDimensions({ width: 1200, height: 500 })
|
|
|
|
// Check the settings were reset
|
|
const storedSettings = TOML.parse(
|
|
await page.evaluate(
|
|
({ settingsKey }) => localStorage.getItem(settingsKey) || '',
|
|
{ settingsKey: TEST_SETTINGS_KEY }
|
|
)
|
|
) as { settings: SaveSettingsPayload }
|
|
|
|
expect(storedSettings.settings?.app?.theme).toBe('dark')
|
|
|
|
// Check that the invalid settings were changed to good defaults
|
|
expect(storedSettings.settings?.modeling?.defaultUnit).toBe('in')
|
|
expect(storedSettings.settings?.modeling?.mouseControls).toBe('Zoo')
|
|
expect(storedSettings.settings?.app?.projectDirectory).toBe('')
|
|
expect(storedSettings.settings?.projects?.defaultProjectName).toBe(
|
|
'project-$nnn'
|
|
)
|
|
}
|
|
)
|
|
|
|
// The behavior is actually broken. Parent always takes precedence
|
|
test.fixme(
|
|
'Project settings can be set and override user settings',
|
|
async ({ page, homePage }) => {
|
|
const u = await getUtils(page)
|
|
await test.step(`Setup`, async () => {
|
|
await page.setBodyDimensions({ width: 1200, height: 500 })
|
|
await homePage.goToModelingScene()
|
|
await page
|
|
.getByRole('button', { name: 'Start Sketch' })
|
|
.waitFor({ state: 'visible' })
|
|
})
|
|
|
|
// Selectors and constants
|
|
const paneButtonLocator = page.getByTestId('debug-pane-button')
|
|
const headingLocator = page.getByRole('heading', {
|
|
name: 'Settings',
|
|
exact: true,
|
|
})
|
|
const inputLocator = page.locator('input[name="modeling-showDebugPanel"]')
|
|
|
|
await test.step('Open settings dialog and set "Show debug panel" to on', async () => {
|
|
await page.keyboard.press('ControlOrMeta+,')
|
|
await expect(headingLocator).toBeVisible()
|
|
|
|
/** Test to close https://github.com/KittyCAD/modeling-app/issues/2713 */
|
|
await test.step(`Confirm that this dialog has a solid background`, async () => {
|
|
await expect
|
|
.poll(
|
|
() => u.getGreatestPixDiff({ x: 600, y: 250 }, [28, 28, 28]),
|
|
{
|
|
timeout: 1000,
|
|
message:
|
|
'Checking for solid background, should not see default plane colors',
|
|
}
|
|
)
|
|
.toBeLessThan(15)
|
|
})
|
|
|
|
await page.locator('#showDebugPanel').getByText('OffOn').click()
|
|
})
|
|
|
|
// Close it and open again with keyboard shortcut, while KCL editor is focused
|
|
// Put the cursor in the editor
|
|
await test.step('Open settings with keyboard shortcut', async () => {
|
|
await page.getByTestId('settings-close-button').click()
|
|
await page.locator('.cm-content').click()
|
|
await page.keyboard.press('ControlOrMeta+,')
|
|
await expect(headingLocator).toBeVisible()
|
|
})
|
|
|
|
// Verify the toast appeared
|
|
await expect(
|
|
page.getByText(`Set show debug panel to "false" for this project`)
|
|
).toBeVisible()
|
|
await expect(
|
|
page.getByText(`Set show debug panel to "false" for this project`)
|
|
).not.toBeVisible()
|
|
|
|
// Check that the debug panel button is gone
|
|
await expect(paneButtonLocator).not.toBeVisible()
|
|
|
|
// Check that the user setting was not changed
|
|
await page.getByRole('radio', { name: 'User' }).click()
|
|
await expect(inputLocator).toBeChecked()
|
|
|
|
// Roll back to default of "off"
|
|
await await page
|
|
.getByText(
|
|
'show debug panelRoll back show debug panelRoll back to match'
|
|
)
|
|
.hover()
|
|
await page
|
|
.getByRole('button', {
|
|
name: 'Roll back show debug panel',
|
|
})
|
|
.click()
|
|
await expect(inputLocator).not.toBeChecked()
|
|
|
|
// Check that the project setting did not change
|
|
await page.getByRole('radio', { name: 'Project' }).click()
|
|
await expect(
|
|
page.locator('input[name="modeling-showDebugPanel"]')
|
|
).not.toBeChecked()
|
|
}
|
|
)
|
|
|
|
test('Keybindings display the correct hotkey for Command Palette', async ({
|
|
page,
|
|
homePage,
|
|
}) => {
|
|
// TODO: fix this test on windows after the electron migration
|
|
test.skip(process.platform === 'win32', 'Skip on windows')
|
|
const u = await getUtils(page)
|
|
await page.setBodyDimensions({ width: 1200, height: 500 })
|
|
await homePage.goToModelingScene()
|
|
await u.waitForPageLoad()
|
|
|
|
await test.step('Open keybindings settings', async () => {
|
|
// Open the settings modal with the keyboard shortcut
|
|
await page.keyboard.press('ControlOrMeta+,')
|
|
|
|
// Go to Keybindings tab.
|
|
const keybindingsTab = page.getByRole('radio', { name: 'Keybindings' })
|
|
await keybindingsTab.click()
|
|
})
|
|
|
|
// Go to the hotkey for Command Palette.
|
|
const commandPalette = page.getByText('Toggle Command Palette')
|
|
await commandPalette.scrollIntoViewIfNeeded()
|
|
|
|
// The heading is above it and should be in view now.
|
|
const commandPaletteHeading = page.getByRole('heading', {
|
|
name: 'Command Palette',
|
|
})
|
|
// The hotkey is in a kbd element next to the heading.
|
|
const hotkey = commandPaletteHeading.locator('+ div kbd')
|
|
const text = process.platform === 'darwin' ? 'Command+K' : 'Control+K'
|
|
await expect(hotkey).toHaveText(text)
|
|
})
|
|
|
|
test.fixme(
|
|
'Project and user settings can be reset',
|
|
async ({ page, homePage }) => {
|
|
const u = await getUtils(page)
|
|
await test.step(`Setup`, async () => {
|
|
await page.setBodyDimensions({ width: 1200, height: 500 })
|
|
await homePage.goToModelingScene()
|
|
await u.waitForPageLoad()
|
|
await page.waitForTimeout(1000)
|
|
})
|
|
|
|
// Selectors and constants
|
|
const projectSettingsTab = page.getByRole('radio', { name: 'Project' })
|
|
const userSettingsTab = page.getByRole('radio', { name: 'User' })
|
|
const resetButton = (level: SettingsLevel) =>
|
|
page.getByRole('button', {
|
|
name: `Reset ${level}-level settings`,
|
|
})
|
|
const themeColorSetting = page.locator('#themeColor').getByRole('slider')
|
|
const settingValues = {
|
|
default: '259',
|
|
user: '120',
|
|
project: '50',
|
|
}
|
|
const resetToast = (level: SettingsLevel) =>
|
|
page.getByText(`${level}-level settings were reset`)
|
|
|
|
await test.step(`Open the settings modal`, async () => {
|
|
await page.getByRole('link', { name: 'Settings' }).last().click()
|
|
await expect(
|
|
page.getByRole('heading', { name: 'Settings', exact: true })
|
|
).toBeVisible()
|
|
})
|
|
|
|
await test.step('Set up theme color', async () => {
|
|
// Verify we're looking at the project-level settings,
|
|
// and it's set to default value
|
|
await expect(projectSettingsTab).toBeChecked()
|
|
await expect(themeColorSetting).toHaveValue(settingValues.default)
|
|
|
|
// Set project-level value to 50
|
|
await themeColorSetting.fill(settingValues.project)
|
|
|
|
// Set user-level value to 120
|
|
await userSettingsTab.click()
|
|
await themeColorSetting.fill(settingValues.user)
|
|
await projectSettingsTab.click()
|
|
})
|
|
|
|
await test.step('Reset project settings', async () => {
|
|
// Click the reset settings button.
|
|
await resetButton('project').click()
|
|
|
|
await expect(resetToast('project')).toBeVisible()
|
|
await expect(resetToast('project')).not.toBeVisible()
|
|
|
|
// Verify it is now set to the inherited user value
|
|
await expect(themeColorSetting).toHaveValue(settingValues.user)
|
|
|
|
await test.step(`Check that the user settings did not change`, async () => {
|
|
await userSettingsTab.click()
|
|
await expect(themeColorSetting).toHaveValue(settingValues.user)
|
|
})
|
|
|
|
await test.step(`Set project-level again to test the user-level reset`, async () => {
|
|
await projectSettingsTab.click()
|
|
await themeColorSetting.fill(settingValues.project)
|
|
await userSettingsTab.click()
|
|
})
|
|
})
|
|
|
|
await test.step('Reset user settings', async () => {
|
|
// Click the reset settings button.
|
|
await resetButton('user').click()
|
|
|
|
await expect(resetToast('user')).toBeVisible()
|
|
await expect(resetToast('user')).not.toBeVisible()
|
|
|
|
// Verify it is now set to the default value
|
|
await expect(themeColorSetting).toHaveValue(settingValues.default)
|
|
|
|
await test.step(`Check that the project settings did not change`, async () => {
|
|
await projectSettingsTab.click()
|
|
await expect(themeColorSetting).toHaveValue(settingValues.project)
|
|
})
|
|
})
|
|
}
|
|
)
|
|
|
|
test.fixme(
|
|
`Project settings override user settings on desktop`,
|
|
{ tag: ['@electron', '@skipWin'] },
|
|
async ({ context, page }, testInfo) => {
|
|
test.skip(
|
|
process.platform === 'win32',
|
|
'TODO: remove this skip https://github.com/KittyCAD/modeling-app/issues/3557'
|
|
)
|
|
const projectName = 'bracket'
|
|
const { dir: projectDirName } = await context.folderSetupFn(
|
|
async (dir) => {
|
|
const bracketDir = join(dir, projectName)
|
|
await fsp.mkdir(bracketDir, { recursive: true })
|
|
await fsp.copyFile(
|
|
executorInputPath('focusrite_scarlett_mounting_braket.kcl'),
|
|
join(bracketDir, 'main.kcl')
|
|
)
|
|
}
|
|
)
|
|
|
|
await page.setBodyDimensions({ width: 1200, height: 500 })
|
|
|
|
// Selectors and constants
|
|
const tempProjectSettingsFilePath = join(
|
|
projectDirName,
|
|
projectName,
|
|
PROJECT_SETTINGS_FILE_NAME
|
|
)
|
|
const tempUserSettingsFilePath = join(projectDirName, SETTINGS_FILE_NAME)
|
|
const userThemeColor = '120'
|
|
const projectThemeColor = '50'
|
|
const settingsOpenButton = page.getByRole('link', {
|
|
name: 'settings Settings',
|
|
})
|
|
const themeColorSetting = page.locator('#themeColor').getByRole('slider')
|
|
const projectSettingsTab = page.getByRole('radio', { name: 'Project' })
|
|
const userSettingsTab = page.getByRole('radio', { name: 'User' })
|
|
const settingsCloseButton = page.getByTestId('settings-close-button')
|
|
const projectLink = page.getByText('bracket')
|
|
const logoLink = page.getByTestId('app-logo')
|
|
|
|
await test.step('Set user theme color on home', async () => {
|
|
await expect(settingsOpenButton).toBeVisible()
|
|
await settingsOpenButton.click()
|
|
// The user tab should be selected by default on home
|
|
await expect(userSettingsTab).toBeChecked()
|
|
await themeColorSetting.fill(userThemeColor)
|
|
await expect(logoLink).toHaveCSS('--primary-hue', userThemeColor)
|
|
await settingsCloseButton.click()
|
|
await expect
|
|
.poll(async () => fsp.readFile(tempUserSettingsFilePath, 'utf-8'), {
|
|
message: 'Setting should now be written to the file',
|
|
timeout: 5_000,
|
|
})
|
|
.toContain(`themeColor = "${userThemeColor}"`)
|
|
})
|
|
|
|
await test.step('Set project theme color', async () => {
|
|
// Open the project
|
|
await projectLink.click()
|
|
await settingsOpenButton.click()
|
|
// The project tab should be selected by default within a project
|
|
await expect(projectSettingsTab).toBeChecked()
|
|
await themeColorSetting.fill(projectThemeColor)
|
|
await expect(logoLink).toHaveCSS('--primary-hue', projectThemeColor)
|
|
await settingsCloseButton.click()
|
|
// Make sure that the project settings file has been written to before continuing
|
|
await expect
|
|
.poll(
|
|
async () => fsp.readFile(tempProjectSettingsFilePath, 'utf-8'),
|
|
{
|
|
message: 'Setting should now be written to the file',
|
|
timeout: 5_000,
|
|
}
|
|
)
|
|
.toContain(`themeColor = "${projectThemeColor}"`)
|
|
})
|
|
|
|
await test.step('Refresh the application and see project setting applied', async () => {
|
|
// Make sure we're done navigating before we reload
|
|
await expect(settingsCloseButton).not.toBeVisible()
|
|
|
|
await page.reload({ waitUntil: 'domcontentloaded' })
|
|
await expect(logoLink).toHaveCSS('--primary-hue', projectThemeColor)
|
|
})
|
|
|
|
await test.step(`Navigate back to the home view and see user setting applied`, async () => {
|
|
await logoLink.click()
|
|
await expect(logoLink).toHaveCSS('--primary-hue', userThemeColor)
|
|
})
|
|
}
|
|
)
|
|
|
|
test(
|
|
`Load desktop app with no settings file`,
|
|
{
|
|
tag: '@electron',
|
|
// This is what makes no settings file get created
|
|
cleanProjectDir: false,
|
|
},
|
|
async ({ page }, testInfo) => {
|
|
await page.setBodyDimensions({ width: 1200, height: 500 })
|
|
|
|
// Selectors and constants
|
|
const errorHeading = page.getByRole('heading', {
|
|
name: 'An unexpected error occurred',
|
|
})
|
|
const projectDirLink = page.getByText('Loaded from')
|
|
|
|
// If the app loads without exploding we're in the clear
|
|
await expect(errorHeading).not.toBeVisible()
|
|
await expect(projectDirLink).toBeVisible()
|
|
}
|
|
)
|
|
|
|
test(
|
|
`Load desktop app with a settings file, but no project directory setting`,
|
|
{
|
|
tag: '@electron',
|
|
appSettings: {
|
|
app: {
|
|
themeColor: '259',
|
|
},
|
|
},
|
|
},
|
|
async ({ context, page }, testInfo) => {
|
|
await page.setBodyDimensions({ width: 1200, height: 500 })
|
|
|
|
// Selectors and constants
|
|
const errorHeading = page.getByRole('heading', {
|
|
name: 'An unexpected error occurred',
|
|
})
|
|
const projectDirLink = page.getByText('Loaded from')
|
|
|
|
// If the app loads without exploding we're in the clear
|
|
await expect(errorHeading).not.toBeVisible()
|
|
await expect(projectDirLink).toBeVisible()
|
|
}
|
|
)
|
|
|
|
// It was much easier to test the logo color than the background stream color.
|
|
test.fixme(
|
|
'user settings reload on external change, on project and modeling view',
|
|
{
|
|
tag: '@electron',
|
|
appSettings: {
|
|
app: {
|
|
// Doesn't matter what you set it to. It will
|
|
// default to 264.5
|
|
themeColor: '0',
|
|
},
|
|
},
|
|
},
|
|
async ({ context, page }, testInfo) => {
|
|
const { dir: projectDirName } = await context.folderSetupFn(
|
|
async () => {}
|
|
)
|
|
|
|
await page.setBodyDimensions({ width: 1200, height: 500 })
|
|
|
|
const logoLink = page.getByTestId('app-logo')
|
|
const projectDirLink = page.getByText('Loaded from')
|
|
|
|
await test.step('Wait for project view', async () => {
|
|
await expect(projectDirLink).toBeVisible()
|
|
await expect(logoLink).toHaveCSS('--primary-hue', '264.5')
|
|
})
|
|
|
|
const changeColor = async (color: string) => {
|
|
const tempSettingsFilePath = join(projectDirName, SETTINGS_FILE_NAME)
|
|
let tomlStr = await fsp.readFile(tempSettingsFilePath, 'utf-8')
|
|
tomlStr = tomlStr.replace(/(themeColor = ")[0-9]+(")/, `$1${color}$2`)
|
|
await fsp.writeFile(tempSettingsFilePath, tomlStr)
|
|
}
|
|
|
|
await test.step('Check color of logo changed', async () => {
|
|
await changeColor('99')
|
|
await expect(logoLink).toHaveCSS('--primary-hue', '99')
|
|
})
|
|
|
|
await test.step('Check color of logo changed when in modeling view', async () => {
|
|
await createProject({ name: 'project-000', page })
|
|
await changeColor('58')
|
|
await expect(logoLink).toHaveCSS('--primary-hue', '58')
|
|
})
|
|
|
|
await test.step('Check going back to projects view still changes the color', async () => {
|
|
await logoLink.click()
|
|
await expect(projectDirLink).toBeVisible()
|
|
await changeColor('21')
|
|
await expect(logoLink).toHaveCSS('--primary-hue', '21')
|
|
})
|
|
}
|
|
)
|
|
|
|
test.fixme(
|
|
'project settings reload on external change',
|
|
{ tag: '@electron' },
|
|
async ({ context, page }, testInfo) => {
|
|
const { dir: projectDirName } = await context.folderSetupFn(
|
|
async () => {}
|
|
)
|
|
|
|
await page.setBodyDimensions({ width: 1200, height: 500 })
|
|
|
|
const logoLink = page.getByTestId('app-logo')
|
|
const projectDirLink = page.getByText('Loaded from')
|
|
|
|
await test.step('Wait for project view', async () => {
|
|
await expect(projectDirLink).toBeVisible()
|
|
})
|
|
|
|
await createProject({ name: 'project-000', page })
|
|
|
|
const changeColorFs = async (color: string) => {
|
|
const tempSettingsFilePath = join(
|
|
projectDirName,
|
|
'project-000',
|
|
PROJECT_SETTINGS_FILE_NAME
|
|
)
|
|
await fsp.writeFile(
|
|
tempSettingsFilePath,
|
|
`[settings.app]\nthemeColor = "${color}"`
|
|
)
|
|
}
|
|
|
|
await test.step('Check the color is first starting as we expect', async () => {
|
|
await expect(logoLink).toHaveCSS('--primary-hue', '264.5')
|
|
})
|
|
|
|
await test.step('Check color of logo changed', async () => {
|
|
await changeColorFs('99')
|
|
await expect(logoLink).toHaveCSS('--primary-hue', '99')
|
|
})
|
|
}
|
|
)
|
|
|
|
test(
|
|
`Closing settings modal should go back to the original file being viewed`,
|
|
{ tag: '@electron' },
|
|
async ({ context, page }, testInfo) => {
|
|
// TODO: fix this test on windows after the electron migration
|
|
test.skip(process.platform === 'win32', 'Skip on windows')
|
|
await context.folderSetupFn(async (dir) => {
|
|
const bracketDir = join(dir, 'project-000')
|
|
await fsp.mkdir(bracketDir, { recursive: true })
|
|
await fsp.copyFile(
|
|
executorInputPath('cube.kcl'),
|
|
join(bracketDir, 'main.kcl')
|
|
)
|
|
await fsp.copyFile(
|
|
executorInputPath('cylinder.kcl'),
|
|
join(bracketDir, '2.kcl')
|
|
)
|
|
})
|
|
const kclCube = await fsp.readFile(executorInputPath('cube.kcl'), 'utf-8')
|
|
const kclCylinder = await fsp.readFile(
|
|
executorInputPath('cylinder.kcl'),
|
|
'utf8'
|
|
)
|
|
|
|
const {
|
|
openKclCodePanel,
|
|
openFilePanel,
|
|
waitForPageLoad,
|
|
selectFile,
|
|
editorTextMatches,
|
|
} = await getUtils(page, test)
|
|
|
|
await page.setBodyDimensions({ width: 1200, height: 500 })
|
|
page.on('console', console.log)
|
|
|
|
await test.step('Precondition: Open to second project file', async () => {
|
|
await expect(page.getByTestId('home-section')).toBeVisible()
|
|
await page.getByText('project-000').click()
|
|
await waitForPageLoad()
|
|
await openKclCodePanel()
|
|
await openFilePanel()
|
|
await editorTextMatches(kclCube)
|
|
|
|
await selectFile('2.kcl')
|
|
await editorTextMatches(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 expect(
|
|
page.getByRole('heading', { name: 'Settings', exact: true })
|
|
).toBeVisible()
|
|
await settingsCloseButton.click()
|
|
})
|
|
|
|
await test.step('Postcondition: Same file content is in editor as before settings opened', async () => {
|
|
await editorTextMatches(kclCylinder)
|
|
})
|
|
}
|
|
)
|
|
|
|
test('Changing modeling default unit', async ({ page, homePage }) => {
|
|
await test.step(`Test setup`, async () => {
|
|
// TODO: fix this test on windows after the electron migration
|
|
test.skip(process.platform === 'win32', 'Skip on windows')
|
|
await page.setBodyDimensions({ width: 1200, height: 500 })
|
|
await homePage.goToModelingScene()
|
|
const toastMessage = page.getByText(`Successfully created "testDefault"`)
|
|
await expect(toastMessage).not.toBeVisible()
|
|
await page
|
|
.getByRole('button', { name: 'Start Sketch' })
|
|
.waitFor({ state: 'visible' })
|
|
})
|
|
|
|
// Selectors and constants
|
|
const userSettingsTab = page.getByRole('radio', { name: 'User' })
|
|
const projectSettingsTab = page.getByRole('radio', { name: 'Project' })
|
|
const defaultUnitSection = page.getByText(
|
|
'default unitRoll back default unitRoll back to match'
|
|
)
|
|
const defaultUnitRollbackButton = page.getByRole('button', {
|
|
name: 'Roll back default unit',
|
|
})
|
|
|
|
await test.step(`Open the settings modal`, async () => {
|
|
await page.getByRole('link', { name: 'Settings' }).last().click()
|
|
await expect(
|
|
page.getByRole('heading', { name: 'Settings', exact: true })
|
|
).toBeVisible()
|
|
})
|
|
|
|
await test.step(`Reset unit setting`, async () => {
|
|
await userSettingsTab.click()
|
|
await defaultUnitSection.hover()
|
|
await defaultUnitRollbackButton.click()
|
|
await projectSettingsTab.hover()
|
|
await projectSettingsTab.click()
|
|
await page.waitForTimeout(1000)
|
|
})
|
|
|
|
await test.step('Change modeling default unit within project tab', async () => {
|
|
const changeUnitOfMeasureInProjectTab = async (unitOfMeasure: string) => {
|
|
await test.step(`Set modeling default unit to ${unitOfMeasure}`, async () => {
|
|
await page
|
|
.getByTestId('modeling-defaultUnit')
|
|
.selectOption(`${unitOfMeasure}`)
|
|
const toastMessage = page.getByText(
|
|
`Set default unit to "${unitOfMeasure}" for this project`
|
|
)
|
|
|
|
// Assert visibility and disapperance
|
|
await expect(toastMessage).toBeVisible()
|
|
await expect(toastMessage).not.toBeVisible()
|
|
})
|
|
}
|
|
await changeUnitOfMeasureInProjectTab('in')
|
|
await changeUnitOfMeasureInProjectTab('ft')
|
|
await changeUnitOfMeasureInProjectTab('yd')
|
|
await changeUnitOfMeasureInProjectTab('mm')
|
|
await changeUnitOfMeasureInProjectTab('cm')
|
|
await changeUnitOfMeasureInProjectTab('m')
|
|
})
|
|
|
|
// Go to the user tab
|
|
await userSettingsTab.hover()
|
|
await userSettingsTab.click()
|
|
await page.waitForTimeout(1000)
|
|
|
|
await test.step('Change modeling default unit within user tab', async () => {
|
|
const changeUnitOfMeasureInUserTab = async (unitOfMeasure: string) => {
|
|
await test.step(`Set modeling default unit to ${unitOfMeasure}`, async () => {
|
|
await page
|
|
.getByTestId('modeling-defaultUnit')
|
|
.selectOption(`${unitOfMeasure}`)
|
|
const toastMessage = page.getByText(
|
|
`Set default unit to "${unitOfMeasure}" as a user default`
|
|
)
|
|
await expect(toastMessage).toBeVisible()
|
|
})
|
|
}
|
|
await changeUnitOfMeasureInUserTab('in')
|
|
await changeUnitOfMeasureInUserTab('ft')
|
|
await changeUnitOfMeasureInUserTab('yd')
|
|
await changeUnitOfMeasureInUserTab('mm')
|
|
await changeUnitOfMeasureInUserTab('cm')
|
|
await changeUnitOfMeasureInUserTab('m')
|
|
})
|
|
|
|
// Close settings
|
|
const settingsCloseButton = page.getByTestId('settings-close-button')
|
|
await settingsCloseButton.click()
|
|
|
|
await test.step('Change modeling default unit within command bar', async () => {
|
|
const commands = page.getByRole('button', { name: 'Commands' })
|
|
const changeUnitOfMeasureInCommandBar = async (unitOfMeasure: string) => {
|
|
// Open command bar
|
|
await commands.click()
|
|
const settingsModelingDefaultUnitCommand = page.getByText(
|
|
'Settings · modeling · default unit'
|
|
)
|
|
await settingsModelingDefaultUnitCommand.click()
|
|
|
|
const commandOption = page.getByRole('option', {
|
|
name: unitOfMeasure,
|
|
exact: true,
|
|
})
|
|
await commandOption.click()
|
|
|
|
const toastMessage = page.getByText(
|
|
`Set default unit to "${unitOfMeasure}" for this project`
|
|
)
|
|
await expect(toastMessage).toBeVisible()
|
|
}
|
|
await changeUnitOfMeasureInCommandBar('in')
|
|
await changeUnitOfMeasureInCommandBar('ft')
|
|
await changeUnitOfMeasureInCommandBar('yd')
|
|
await changeUnitOfMeasureInCommandBar('mm')
|
|
await changeUnitOfMeasureInCommandBar('cm')
|
|
await changeUnitOfMeasureInCommandBar('m')
|
|
})
|
|
|
|
await test.step('Change modeling default unit within gizmo', async () => {
|
|
const changeUnitOfMeasureInGizmo = async (
|
|
unitOfMeasure: string,
|
|
copy: string
|
|
) => {
|
|
const gizmo = page.getByRole('button', {
|
|
name: 'Current units are: ',
|
|
})
|
|
await gizmo.click()
|
|
const button = page.getByRole('button', {
|
|
name: copy,
|
|
exact: true,
|
|
})
|
|
await button.click()
|
|
const toastMessage = page.getByText(
|
|
`Set default unit to "${unitOfMeasure}" for this project`
|
|
)
|
|
await expect(toastMessage).toBeVisible()
|
|
}
|
|
|
|
await changeUnitOfMeasureInGizmo('in', 'Inches')
|
|
await changeUnitOfMeasureInGizmo('ft', 'Feet')
|
|
await changeUnitOfMeasureInGizmo('yd', 'Yards')
|
|
await changeUnitOfMeasureInGizmo('mm', 'Millimeters')
|
|
await changeUnitOfMeasureInGizmo('cm', 'Centimeters')
|
|
await changeUnitOfMeasureInGizmo('m', 'Meters')
|
|
})
|
|
})
|
|
|
|
test('Changing theme in sketch mode', async ({ context, page, homePage }) => {
|
|
// TODO: fix this test on windows after the electron migration
|
|
test.skip(process.platform === 'win32', 'Skip on windows')
|
|
const u = await getUtils(page)
|
|
await context.addInitScript(() => {
|
|
localStorage.setItem(
|
|
'persistCode',
|
|
`sketch001 = startSketchOn('XZ')
|
|
|> startProfileAt([0, 0], %)
|
|
|> line(end = [5, 0])
|
|
|> line(end = [0, 5])
|
|
|> line(end = [-5, 0])
|
|
|> line(endAbsolute = [profileStartX(%), profileStartY(%)])
|
|
|> close()
|
|
extrude001 = extrude(sketch001, length = 5)
|
|
`
|
|
)
|
|
})
|
|
await page.setBodyDimensions({ width: 1200, height: 500 })
|
|
await homePage.goToModelingScene()
|
|
await u.waitForPageLoad()
|
|
await page.waitForTimeout(1000)
|
|
|
|
// Selectors and constants
|
|
const editSketchButton = page.getByRole('button', { name: 'Edit Sketch' })
|
|
const lineToolButton = page.getByTestId('line')
|
|
const segmentOverlays = page.getByTestId('segment-overlay')
|
|
const sketchOriginLocation = { x: 600, y: 250 }
|
|
const darkThemeSegmentColor: [number, number, number] = [215, 215, 215]
|
|
const lightThemeSegmentColor: [number, number, number] = [90, 90, 90]
|
|
|
|
await test.step(`Get into sketch mode`, async () => {
|
|
await page.mouse.click(700, 200)
|
|
await expect(editSketchButton).toBeVisible()
|
|
await editSketchButton.click()
|
|
|
|
// We use the line tool as a proxy for sketch mode
|
|
await expect(lineToolButton).toBeVisible()
|
|
await expect(segmentOverlays).toHaveCount(4)
|
|
// but we allow more time to pass for animating to the sketch
|
|
await page.waitForTimeout(1000)
|
|
})
|
|
|
|
await test.step(`Check the sketch line color before`, async () => {
|
|
await expect
|
|
.poll(() =>
|
|
u.getGreatestPixDiff(sketchOriginLocation, darkThemeSegmentColor)
|
|
)
|
|
.toBeLessThan(15)
|
|
})
|
|
|
|
await test.step(`Change theme to light using command palette`, async () => {
|
|
await page.keyboard.press('ControlOrMeta+K')
|
|
await page.getByRole('option', { name: 'theme' }).click()
|
|
await page.getByRole('option', { name: 'light' }).click()
|
|
await expect(page.getByText('theme to "light"')).toBeVisible()
|
|
|
|
// Make sure we haven't left sketch mode
|
|
await expect(lineToolButton).toBeVisible()
|
|
})
|
|
|
|
await test.step(`Check the sketch line color after`, async () => {
|
|
await expect
|
|
.poll(() =>
|
|
u.getGreatestPixDiff(sketchOriginLocation, lightThemeSegmentColor)
|
|
)
|
|
.toBeLessThan(15)
|
|
})
|
|
})
|
|
|
|
test(
|
|
`Changing system theme preferences (via media query) should update UI and stream`,
|
|
{
|
|
// Override the settings so that the theme is set to `system`
|
|
appSettings: TEST_SETTINGS_DEFAULT_THEME,
|
|
},
|
|
async ({ page, homePage }) => {
|
|
const u = await getUtils(page)
|
|
|
|
// Selectors and constants
|
|
const darkBackgroundCss = 'oklch(0.3012 0 264.5)'
|
|
const lightBackgroundCss = 'oklch(0.9911 0 264.5)'
|
|
const darkBackgroundColor: [number, number, number] = [27, 27, 27]
|
|
const lightBackgroundColor: [number, number, number] = [245, 245, 245]
|
|
const streamBackgroundPixelIsColor = async (
|
|
color: [number, number, number]
|
|
) => {
|
|
return u.getGreatestPixDiff({ x: 1000, y: 200 }, color)
|
|
}
|
|
const toolbar = page.locator('menu').filter({ hasText: 'Start Sketch' })
|
|
|
|
await test.step(`Test setup`, async () => {
|
|
await page.setBodyDimensions({ width: 1200, height: 500 })
|
|
await homePage.goToModelingScene()
|
|
await u.waitForPageLoad()
|
|
await page.waitForTimeout(1000)
|
|
await expect(toolbar).toBeVisible()
|
|
})
|
|
|
|
await test.step(`Check the background color is light before`, async () => {
|
|
await expect(toolbar).toHaveCSS('background-color', lightBackgroundCss)
|
|
await expect
|
|
.poll(() => streamBackgroundPixelIsColor(lightBackgroundColor))
|
|
.toBeLessThan(15)
|
|
})
|
|
|
|
await test.step(`Change media query preference to dark, emulating dusk with system theme`, async () => {
|
|
await page.emulateMedia({ colorScheme: 'dark' })
|
|
})
|
|
|
|
await test.step(`Check the background color is dark after`, async () => {
|
|
await expect(toolbar).toHaveCSS('background-color', darkBackgroundCss)
|
|
await expect
|
|
.poll(() => streamBackgroundPixelIsColor(darkBackgroundColor))
|
|
.toBeLessThan(15)
|
|
})
|
|
}
|
|
)
|
|
|
|
test(
|
|
`Turning off "Show debug panel" with debug panel open leaves no phantom panel`,
|
|
{
|
|
// Override beforeEach test setup
|
|
// with debug panel open
|
|
// but "show debug panel" set to false
|
|
appSettings: {
|
|
...TEST_SETTINGS,
|
|
modeling: { ...TEST_SETTINGS.modeling, showDebugPanel: false },
|
|
},
|
|
},
|
|
async ({ context, page, homePage }) => {
|
|
const u = await getUtils(page)
|
|
|
|
await context.addInitScript(async () => {
|
|
localStorage.setItem(
|
|
'persistModelingContext',
|
|
'{"openPanes":["debug"]}'
|
|
)
|
|
})
|
|
await page.setBodyDimensions({ width: 1200, height: 500 })
|
|
await homePage.goToModelingScene()
|
|
|
|
// Constants and locators
|
|
const resizeHandle = page.locator('.sidebar-resize-handles > div.block')
|
|
const debugPaneButton = page.getByTestId('debug-pane-button')
|
|
const commandsButton = page.getByRole('button', { name: 'Commands' })
|
|
const debugPaneOption = page.getByRole('option', {
|
|
name: 'Settings · modeling · show debug panel',
|
|
})
|
|
|
|
async function setShowDebugPanelTo(value: 'On' | 'Off') {
|
|
await commandsButton.click()
|
|
await debugPaneOption.click()
|
|
await page.getByRole('option', { name: value }).click()
|
|
await expect(
|
|
page.getByText(
|
|
`Set show debug panel to "${value === 'On'}" for this project`
|
|
)
|
|
).toBeVisible()
|
|
}
|
|
|
|
await test.step(`Initial load with corrupted settings`, async () => {
|
|
// Check that the debug panel is not visible
|
|
await expect(debugPaneButton).not.toBeVisible()
|
|
// Check the pane resize handle wrapper is not visible
|
|
await expect(resizeHandle).not.toBeVisible()
|
|
})
|
|
|
|
await test.step(`Open code pane to verify we see the resize handles`, async () => {
|
|
await u.openKclCodePanel()
|
|
await expect(resizeHandle).toBeVisible()
|
|
await u.closeKclCodePanel()
|
|
})
|
|
|
|
await test.step(`Turn on debug panel, open it`, async () => {
|
|
await setShowDebugPanelTo('On')
|
|
await expect(debugPaneButton).toBeVisible()
|
|
// We want the logic to clear the phantom panel, so we shouldn't see
|
|
// the real panel (and therefore the resize handle) yet
|
|
await expect(resizeHandle).not.toBeVisible()
|
|
await u.openDebugPanel()
|
|
await expect(resizeHandle).toBeVisible()
|
|
})
|
|
|
|
await test.step(`Turn off debug panel setting with it open`, async () => {
|
|
await setShowDebugPanelTo('Off')
|
|
await expect(debugPaneButton).not.toBeVisible()
|
|
await expect(resizeHandle).not.toBeVisible()
|
|
})
|
|
}
|
|
)
|
|
})
|