Merge branch 'main' into coredump-clientstate
This commit is contained in:
@ -1,5 +1,5 @@
|
|||||||
import { test, expect, Page } from '@playwright/test'
|
import { test, expect, Page } from '@playwright/test'
|
||||||
import { makeTemplate, getUtils } from './test-utils'
|
import { makeTemplate, getUtils, doExport } from './test-utils'
|
||||||
import waitOn from 'wait-on'
|
import waitOn from 'wait-on'
|
||||||
import { roundOff, uuidv4 } from 'lib/utils'
|
import { roundOff, uuidv4 } from 'lib/utils'
|
||||||
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
|
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
|
||||||
@ -2439,6 +2439,158 @@ test('Extrude from command bar selects extrude line after', async ({
|
|||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test.describe('Testing constraints', () => {
|
||||||
|
test.describe('Two segment - no modal constraints', () => {
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
codeAfter: `|> angledLine([83, segLen('seg01', %)], %)`,
|
||||||
|
constraintName: 'Equal Length',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
codeAfter: `|> angledLine([segAng('seg01', %), 78.33], %)`,
|
||||||
|
constraintName: 'Parallel',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
codeAfter: `|> lineTo([segEndX('seg01', %), 61.34], %)`,
|
||||||
|
constraintName: 'Vertically Align',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
codeAfter: `|> lineTo([154.9, segEndY('seg01', %)], %)`,
|
||||||
|
constraintName: 'Horizontally Align',
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
for (const { codeAfter, constraintName } of cases) {
|
||||||
|
test(`${constraintName}`, async ({ page }) => {
|
||||||
|
await page.addInitScript(async () => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'persistCode',
|
||||||
|
`const yo = 5
|
||||||
|
const part001 = startSketchOn('XZ')
|
||||||
|
|> startProfileAt([-7.54, -26.74], %)
|
||||||
|
|> line([74.36, 130.4], %)
|
||||||
|
|> line([78.92, -120.11], %)
|
||||||
|
|> line([9.16, 77.79], %)
|
||||||
|
const part002 = startSketchOn('XZ')
|
||||||
|
|> startProfileAt([299.05, 231.45], %)
|
||||||
|
|> xLine(-425.34, %, 'seg-what')
|
||||||
|
|> yLine(-264.06, %)
|
||||||
|
|> xLine(segLen('seg-what', %), %)
|
||||||
|
|> lineTo([profileStartX(%), profileStartY(%)], %)`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
const u = await getUtils(page)
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
|
await page.getByText('line([74.36, 130.4], %)').click()
|
||||||
|
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||||
|
|
||||||
|
const line1 = await u.getBoundingBox(`[data-overlay-index="${0}"]`)
|
||||||
|
const line3 = await u.getBoundingBox(`[data-overlay-index="${2}"]`)
|
||||||
|
|
||||||
|
// select two segments by holding down shift
|
||||||
|
await page.mouse.click(line1.x - 20, line1.y + 20)
|
||||||
|
await page.keyboard.down('Shift')
|
||||||
|
await page.mouse.click(line3.x - 3, line3.y + 20)
|
||||||
|
await page.keyboard.up('Shift')
|
||||||
|
const constraintMenuButton = page.getByRole('button', {
|
||||||
|
name: 'Constrain',
|
||||||
|
})
|
||||||
|
const constraintButton = page.getByRole('button', {
|
||||||
|
name: constraintName,
|
||||||
|
})
|
||||||
|
|
||||||
|
// apply the constraint
|
||||||
|
await constraintMenuButton.click()
|
||||||
|
await constraintButton.click()
|
||||||
|
|
||||||
|
await expect(page.locator('.cm-content')).toContainText(codeAfter)
|
||||||
|
// expect the string 'seg01' to appear twice in '.cm-content' the tag segment and referencing the tag
|
||||||
|
const content = await page.locator('.cm-content').innerText()
|
||||||
|
await expect(content.match(/seg01/g)).toHaveLength(2)
|
||||||
|
// check there are still 2 cursors (they should stay on the same lines as before constraint was applied)
|
||||||
|
await expect(page.locator('.cm-cursor')).toHaveCount(2)
|
||||||
|
// check actives lines
|
||||||
|
const activeLinesContent = await page.locator('.cm-activeLine').all()
|
||||||
|
await expect(activeLinesContent).toHaveLength(2)
|
||||||
|
|
||||||
|
// check both cursors are where they should be after constraint is applied
|
||||||
|
await expect(activeLinesContent[0]).toHaveText(
|
||||||
|
"|> line([74.36, 130.4], %, 'seg01')"
|
||||||
|
)
|
||||||
|
await expect(activeLinesContent[1]).toHaveText(codeAfter)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
test.describe('Axis & segment - no modal constraints', () => {
|
||||||
|
const cases = [
|
||||||
|
{
|
||||||
|
codeAfter: `|> lineTo([154.9, ZERO], %)`,
|
||||||
|
axisClick: { x: 950, y: 250 },
|
||||||
|
constraintName: 'Snap To X',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
codeAfter: `|> lineTo([ZERO, 61.34], %)`,
|
||||||
|
axisClick: { x: 600, y: 150 },
|
||||||
|
constraintName: 'Snap To Y',
|
||||||
|
},
|
||||||
|
] as const
|
||||||
|
for (const { codeAfter, constraintName, axisClick } of cases) {
|
||||||
|
test(`${constraintName}`, async ({ page }) => {
|
||||||
|
await page.addInitScript(async () => {
|
||||||
|
localStorage.setItem(
|
||||||
|
'persistCode',
|
||||||
|
`const yo = 5
|
||||||
|
const part001 = startSketchOn('XZ')
|
||||||
|
|> startProfileAt([-7.54, -26.74], %)
|
||||||
|
|> line([74.36, 130.4], %)
|
||||||
|
|> line([78.92, -120.11], %)
|
||||||
|
|> line([9.16, 77.79], %)
|
||||||
|
const part002 = startSketchOn('XZ')
|
||||||
|
|> startProfileAt([299.05, 231.45], %)
|
||||||
|
|> xLine(-425.34, %, 'seg-what')
|
||||||
|
|> yLine(-264.06, %)
|
||||||
|
|> xLine(segLen('seg-what', %), %)
|
||||||
|
|> lineTo([profileStartX(%), profileStartY(%)], %)`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
const u = await getUtils(page)
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
|
||||||
|
await page.getByText('line([74.36, 130.4], %)').click()
|
||||||
|
await page.getByRole('button', { name: 'Edit Sketch' }).click()
|
||||||
|
|
||||||
|
const line3 = await u.getBoundingBox(`[data-overlay-index="${2}"]`)
|
||||||
|
|
||||||
|
// select segment and axis by holding down shift
|
||||||
|
await page.mouse.click(line3.x - 3, line3.y + 20)
|
||||||
|
await page.keyboard.down('Shift')
|
||||||
|
await page.waitForTimeout(100)
|
||||||
|
await page.mouse.click(axisClick.x, axisClick.y)
|
||||||
|
await page.keyboard.up('Shift')
|
||||||
|
const constraintMenuButton = page.getByRole('button', {
|
||||||
|
name: 'Constrain',
|
||||||
|
})
|
||||||
|
const constraintButton = page.getByRole('button', {
|
||||||
|
name: constraintName,
|
||||||
|
})
|
||||||
|
|
||||||
|
// apply the constraint
|
||||||
|
await constraintMenuButton.click()
|
||||||
|
await expect(constraintButton).toBeVisible()
|
||||||
|
await constraintButton.click()
|
||||||
|
|
||||||
|
// check the cursor is where is should be after constraint is applied
|
||||||
|
await expect(page.locator('.cm-content')).toContainText(codeAfter)
|
||||||
|
await expect(page.locator('.cm-activeLine')).toHaveText(codeAfter)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
test.describe('Testing segment overlays', () => {
|
test.describe('Testing segment overlays', () => {
|
||||||
test.describe('Hover over a segment should show its overlay, hovering over the input overlays should show its popover, clicking the input overlay should constrain/unconstrain it:\nfor the following segments', () => {
|
test.describe('Hover over a segment should show its overlay, hovering over the input overlays should show its popover, clicking the input overlay should constrain/unconstrain it:\nfor the following segments', () => {
|
||||||
/**
|
/**
|
||||||
@ -3811,3 +3963,75 @@ test('Engine disconnect & reconnect in sketch mode', async ({ page }) => {
|
|||||||
page.getByRole('button', { name: 'Exit Sketch' })
|
page.getByRole('button', { name: 'Exit Sketch' })
|
||||||
).not.toBeVisible()
|
).not.toBeVisible()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('Successful export shows a success toast', async ({ page }) => {
|
||||||
|
// FYI this test doesn't work with only engine running locally
|
||||||
|
// And you will need to have the KittyCAD CLI installed
|
||||||
|
const u = await getUtils(page)
|
||||||
|
await page.addInitScript(async () => {
|
||||||
|
;(window as any).playwrightSkipFilePicker = true
|
||||||
|
localStorage.setItem(
|
||||||
|
'persistCode',
|
||||||
|
`const topAng = 25
|
||||||
|
const bottomAng = 35
|
||||||
|
const baseLen = 3.5
|
||||||
|
const baseHeight = 1
|
||||||
|
const totalHeightHalf = 2
|
||||||
|
const armThick = 0.5
|
||||||
|
const totalLen = 9.5
|
||||||
|
const part001 = startSketchOn('-XZ')
|
||||||
|
|> startProfileAt([0, 0], %)
|
||||||
|
|> yLine(baseHeight, %)
|
||||||
|
|> xLine(baseLen, %)
|
||||||
|
|> angledLineToY({
|
||||||
|
angle: topAng,
|
||||||
|
to: totalHeightHalf,
|
||||||
|
}, %, 'seg04')
|
||||||
|
|> xLineTo(totalLen, %, 'seg03')
|
||||||
|
|> yLine(-armThick, %, 'seg01')
|
||||||
|
|> angledLineThatIntersects({
|
||||||
|
angle: HALF_TURN,
|
||||||
|
offset: -armThick,
|
||||||
|
intersectTag: 'seg04'
|
||||||
|
}, %)
|
||||||
|
|> angledLineToY([segAng('seg04', %) + 180, ZERO], %)
|
||||||
|
|> angledLineToY({
|
||||||
|
angle: -bottomAng,
|
||||||
|
to: -totalHeightHalf - armThick,
|
||||||
|
}, %, 'seg02')
|
||||||
|
|> xLineTo(segEndX('seg03', %) + 0, %)
|
||||||
|
|> yLine(-segLen('seg01', %), %)
|
||||||
|
|> angledLineThatIntersects({
|
||||||
|
angle: HALF_TURN,
|
||||||
|
offset: -armThick,
|
||||||
|
intersectTag: 'seg02'
|
||||||
|
}, %)
|
||||||
|
|> angledLineToY([segAng('seg02', %) + 180, -baseHeight], %)
|
||||||
|
|> xLineTo(ZERO, %)
|
||||||
|
|> close(%)
|
||||||
|
|> extrude(4, %)`
|
||||||
|
)
|
||||||
|
})
|
||||||
|
await page.setViewportSize({ width: 1200, height: 500 })
|
||||||
|
await page.goto('/')
|
||||||
|
await u.waitForAuthSkipAppStart()
|
||||||
|
await u.openDebugPanel()
|
||||||
|
await u.expectCmdLog('[data-message-type="execution-done"]')
|
||||||
|
await u.waitForCmdReceive('extrude')
|
||||||
|
await page.waitForTimeout(1000)
|
||||||
|
await u.clearAndCloseDebugPanel()
|
||||||
|
|
||||||
|
await doExport(
|
||||||
|
{
|
||||||
|
type: 'gltf',
|
||||||
|
storage: 'embedded',
|
||||||
|
presentation: 'pretty',
|
||||||
|
},
|
||||||
|
page
|
||||||
|
)
|
||||||
|
|
||||||
|
// This is the main thing we're testing,
|
||||||
|
// We test the export functionality across all
|
||||||
|
// file types in snapshot-tests.spec.ts
|
||||||
|
await expect(page.getByText('Exported successfully')).toBeVisible()
|
||||||
|
})
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { test, expect, Download } from '@playwright/test'
|
import { test, expect } from '@playwright/test'
|
||||||
import { secrets } from './secrets'
|
import { secrets } from './secrets'
|
||||||
import { getUtils } from './test-utils'
|
import { Paths, doExport, getUtils } from './test-utils'
|
||||||
import { Models } from '@kittycad/lib'
|
import { Models } from '@kittycad/lib'
|
||||||
import fsp from 'fs/promises'
|
import fsp from 'fs/promises'
|
||||||
import { spawn } from 'child_process'
|
import { spawn } from 'child_process'
|
||||||
import { APP_NAME, KCL_DEFAULT_LENGTH } from 'lib/constants'
|
import { KCL_DEFAULT_LENGTH } from 'lib/constants'
|
||||||
import JSZip from 'jszip'
|
import JSZip from 'jszip'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { TEST_SETTINGS, TEST_SETTINGS_KEY } from './storageStates'
|
import { TEST_SETTINGS, TEST_SETTINGS_KEY } from './storageStates'
|
||||||
@ -99,78 +99,6 @@ const part001 = startSketchOn('-XZ')
|
|||||||
await page.waitForTimeout(1000)
|
await page.waitForTimeout(1000)
|
||||||
await u.clearAndCloseDebugPanel()
|
await u.clearAndCloseDebugPanel()
|
||||||
|
|
||||||
interface Paths {
|
|
||||||
modelPath: string
|
|
||||||
imagePath: string
|
|
||||||
outputType: string
|
|
||||||
}
|
|
||||||
const doExport = async (
|
|
||||||
output: Models['OutputFormat_type']
|
|
||||||
): Promise<Paths> => {
|
|
||||||
await page.getByRole('button', { name: APP_NAME }).click()
|
|
||||||
await expect(
|
|
||||||
page.getByRole('button', { name: 'Export Part' })
|
|
||||||
).toBeVisible()
|
|
||||||
await page.getByRole('button', { name: 'Export Part' }).click()
|
|
||||||
await expect(page.getByTestId('command-bar')).toBeVisible()
|
|
||||||
|
|
||||||
// Go through export via command bar
|
|
||||||
await page.getByRole('option', { name: output.type, exact: false }).click()
|
|
||||||
await page.locator('#arg-form').waitFor({ state: 'detached' })
|
|
||||||
if ('storage' in output) {
|
|
||||||
await page.getByTestId('arg-name-storage').waitFor({ timeout: 1000 })
|
|
||||||
await page.getByRole('button', { name: 'storage', exact: false }).click()
|
|
||||||
await page
|
|
||||||
.getByRole('option', { name: output.storage, exact: false })
|
|
||||||
.click()
|
|
||||||
await page.locator('#arg-form').waitFor({ state: 'detached' })
|
|
||||||
}
|
|
||||||
await expect(page.getByText('Confirm Export')).toBeVisible()
|
|
||||||
|
|
||||||
const getPromiseAndResolve = () => {
|
|
||||||
let resolve: any = () => {}
|
|
||||||
const promise = new Promise<Download>((r) => {
|
|
||||||
resolve = r
|
|
||||||
})
|
|
||||||
return [promise, resolve]
|
|
||||||
}
|
|
||||||
|
|
||||||
const [downloadPromise1, downloadResolve1] = getPromiseAndResolve()
|
|
||||||
let downloadCnt = 0
|
|
||||||
|
|
||||||
page.on('download', async (download) => {
|
|
||||||
if (downloadCnt === 0) {
|
|
||||||
downloadResolve1(download)
|
|
||||||
}
|
|
||||||
downloadCnt++
|
|
||||||
})
|
|
||||||
await page.getByRole('button', { name: 'Submit command' }).click()
|
|
||||||
|
|
||||||
// Handle download
|
|
||||||
const download = await downloadPromise1
|
|
||||||
const downloadLocationer = (extra = '', isImage = false) =>
|
|
||||||
`./e2e/playwright/export-snapshots/${output.type}-${
|
|
||||||
'storage' in output ? output.storage : ''
|
|
||||||
}${extra}.${isImage ? 'png' : output.type}`
|
|
||||||
const downloadLocation = downloadLocationer()
|
|
||||||
|
|
||||||
await download.saveAs(downloadLocation)
|
|
||||||
|
|
||||||
if (output.type === 'step') {
|
|
||||||
// stable timestamps for step files
|
|
||||||
const fileContents = await fsp.readFile(downloadLocation, 'utf-8')
|
|
||||||
const newFileContents = fileContents.replace(
|
|
||||||
/[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]+[0-9]+[0-9]\+[0-9]{2}:[0-9]{2}/g,
|
|
||||||
'1970-01-01T00:00:00.0+00:00'
|
|
||||||
)
|
|
||||||
await fsp.writeFile(downloadLocation, newFileContents)
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
modelPath: downloadLocation,
|
|
||||||
imagePath: downloadLocationer('', true),
|
|
||||||
outputType: output.type,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const axisDirectionPair: Models['AxisDirectionPair_type'] = {
|
const axisDirectionPair: Models['AxisDirectionPair_type'] = {
|
||||||
axis: 'z',
|
axis: 'z',
|
||||||
direction: 'positive',
|
direction: 'positive',
|
||||||
@ -186,84 +114,114 @@ const part001 = startSketchOn('-XZ')
|
|||||||
// just note that only `type` and `storage` are used for selecting the drop downs is the app
|
// just note that only `type` and `storage` are used for selecting the drop downs is the app
|
||||||
// the rest are only there to make typescript happy
|
// the rest are only there to make typescript happy
|
||||||
exportLocations.push(
|
exportLocations.push(
|
||||||
await doExport({
|
await doExport(
|
||||||
type: 'step',
|
{
|
||||||
coords: sysType,
|
type: 'step',
|
||||||
})
|
coords: sysType,
|
||||||
|
},
|
||||||
|
page
|
||||||
|
)
|
||||||
)
|
)
|
||||||
exportLocations.push(
|
exportLocations.push(
|
||||||
await doExport({
|
await doExport(
|
||||||
type: 'ply',
|
{
|
||||||
coords: sysType,
|
type: 'ply',
|
||||||
selection: { type: 'default_scene' },
|
coords: sysType,
|
||||||
storage: 'ascii',
|
selection: { type: 'default_scene' },
|
||||||
units: 'in',
|
storage: 'ascii',
|
||||||
})
|
units: 'in',
|
||||||
|
},
|
||||||
|
page
|
||||||
|
)
|
||||||
)
|
)
|
||||||
exportLocations.push(
|
exportLocations.push(
|
||||||
await doExport({
|
await doExport(
|
||||||
type: 'ply',
|
{
|
||||||
storage: 'binary_little_endian',
|
type: 'ply',
|
||||||
coords: sysType,
|
storage: 'binary_little_endian',
|
||||||
selection: { type: 'default_scene' },
|
coords: sysType,
|
||||||
units: 'in',
|
selection: { type: 'default_scene' },
|
||||||
})
|
units: 'in',
|
||||||
|
},
|
||||||
|
page
|
||||||
|
)
|
||||||
)
|
)
|
||||||
exportLocations.push(
|
exportLocations.push(
|
||||||
await doExport({
|
await doExport(
|
||||||
type: 'ply',
|
{
|
||||||
storage: 'binary_big_endian',
|
type: 'ply',
|
||||||
coords: sysType,
|
storage: 'binary_big_endian',
|
||||||
selection: { type: 'default_scene' },
|
coords: sysType,
|
||||||
units: 'in',
|
selection: { type: 'default_scene' },
|
||||||
})
|
units: 'in',
|
||||||
|
},
|
||||||
|
page
|
||||||
|
)
|
||||||
)
|
)
|
||||||
exportLocations.push(
|
exportLocations.push(
|
||||||
await doExport({
|
await doExport(
|
||||||
type: 'stl',
|
{
|
||||||
storage: 'ascii',
|
type: 'stl',
|
||||||
coords: sysType,
|
storage: 'ascii',
|
||||||
units: 'in',
|
coords: sysType,
|
||||||
selection: { type: 'default_scene' },
|
units: 'in',
|
||||||
})
|
selection: { type: 'default_scene' },
|
||||||
|
},
|
||||||
|
page
|
||||||
|
)
|
||||||
)
|
)
|
||||||
exportLocations.push(
|
exportLocations.push(
|
||||||
await doExport({
|
await doExport(
|
||||||
type: 'stl',
|
{
|
||||||
storage: 'binary',
|
type: 'stl',
|
||||||
coords: sysType,
|
storage: 'binary',
|
||||||
units: 'in',
|
coords: sysType,
|
||||||
selection: { type: 'default_scene' },
|
units: 'in',
|
||||||
})
|
selection: { type: 'default_scene' },
|
||||||
|
},
|
||||||
|
page
|
||||||
|
)
|
||||||
)
|
)
|
||||||
exportLocations.push(
|
exportLocations.push(
|
||||||
await doExport({
|
await doExport(
|
||||||
// obj seems to be a little flaky, times out tests sometimes
|
{
|
||||||
type: 'obj',
|
// obj seems to be a little flaky, times out tests sometimes
|
||||||
coords: sysType,
|
type: 'obj',
|
||||||
units: 'in',
|
coords: sysType,
|
||||||
})
|
units: 'in',
|
||||||
|
},
|
||||||
|
page
|
||||||
|
)
|
||||||
)
|
)
|
||||||
exportLocations.push(
|
exportLocations.push(
|
||||||
await doExport({
|
await doExport(
|
||||||
type: 'gltf',
|
{
|
||||||
storage: 'embedded',
|
type: 'gltf',
|
||||||
presentation: 'pretty',
|
storage: 'embedded',
|
||||||
})
|
presentation: 'pretty',
|
||||||
|
},
|
||||||
|
page
|
||||||
|
)
|
||||||
)
|
)
|
||||||
exportLocations.push(
|
exportLocations.push(
|
||||||
await doExport({
|
await doExport(
|
||||||
type: 'gltf',
|
{
|
||||||
storage: 'binary',
|
type: 'gltf',
|
||||||
presentation: 'pretty',
|
storage: 'binary',
|
||||||
})
|
presentation: 'pretty',
|
||||||
|
},
|
||||||
|
page
|
||||||
|
)
|
||||||
)
|
)
|
||||||
exportLocations.push(
|
exportLocations.push(
|
||||||
await doExport({
|
await doExport(
|
||||||
type: 'gltf',
|
{
|
||||||
storage: 'standard',
|
type: 'gltf',
|
||||||
presentation: 'pretty',
|
storage: 'standard',
|
||||||
})
|
presentation: 'pretty',
|
||||||
|
},
|
||||||
|
page
|
||||||
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
// close page to disconnect websocket since we can only have one open atm
|
// close page to disconnect websocket since we can only have one open atm
|
||||||
|
|||||||
@ -1,9 +1,11 @@
|
|||||||
import { test, expect, Page } from '@playwright/test'
|
import { test, expect, Page, Download } from '@playwright/test'
|
||||||
import { EngineCommand } from '../../src/lang/std/engineConnection'
|
import { EngineCommand } from '../../src/lang/std/engineConnection'
|
||||||
import fsp from 'fs/promises'
|
import fsp from 'fs/promises'
|
||||||
import pixelMatch from 'pixelmatch'
|
import pixelMatch from 'pixelmatch'
|
||||||
import { PNG } from 'pngjs'
|
import { PNG } from 'pngjs'
|
||||||
import { Protocol } from 'playwright-core/types/protocol'
|
import { Protocol } from 'playwright-core/types/protocol'
|
||||||
|
import type { Models } from '@kittycad/lib'
|
||||||
|
import { APP_NAME } from 'lib/constants'
|
||||||
|
|
||||||
async function waitForPageLoad(page: Page) {
|
async function waitForPageLoad(page: Page) {
|
||||||
// wait for 'Loading stream...' spinner
|
// wait for 'Loading stream...' spinner
|
||||||
@ -277,3 +279,77 @@ export const makeTemplate: (
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Paths {
|
||||||
|
modelPath: string
|
||||||
|
imagePath: string
|
||||||
|
outputType: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export const doExport = async (
|
||||||
|
output: Models['OutputFormat_type'],
|
||||||
|
page: Page
|
||||||
|
): Promise<Paths> => {
|
||||||
|
await page.getByRole('button', { name: APP_NAME }).click()
|
||||||
|
await expect(page.getByRole('button', { name: 'Export Part' })).toBeVisible()
|
||||||
|
await page.getByRole('button', { name: 'Export Part' }).click()
|
||||||
|
await expect(page.getByTestId('command-bar')).toBeVisible()
|
||||||
|
|
||||||
|
// Go through export via command bar
|
||||||
|
await page.getByRole('option', { name: output.type, exact: false }).click()
|
||||||
|
await page.locator('#arg-form').waitFor({ state: 'detached' })
|
||||||
|
if ('storage' in output) {
|
||||||
|
await page.getByTestId('arg-name-storage').waitFor({ timeout: 1000 })
|
||||||
|
await page.getByRole('button', { name: 'storage', exact: false }).click()
|
||||||
|
await page
|
||||||
|
.getByRole('option', { name: output.storage, exact: false })
|
||||||
|
.click()
|
||||||
|
await page.locator('#arg-form').waitFor({ state: 'detached' })
|
||||||
|
}
|
||||||
|
await expect(page.getByText('Confirm Export')).toBeVisible()
|
||||||
|
|
||||||
|
const getPromiseAndResolve = () => {
|
||||||
|
let resolve: any = () => {}
|
||||||
|
const promise = new Promise<Download>((r) => {
|
||||||
|
resolve = r
|
||||||
|
})
|
||||||
|
return [promise, resolve]
|
||||||
|
}
|
||||||
|
|
||||||
|
const [downloadPromise1, downloadResolve1] = getPromiseAndResolve()
|
||||||
|
let downloadCnt = 0
|
||||||
|
|
||||||
|
page.on('download', async (download) => {
|
||||||
|
if (downloadCnt === 0) {
|
||||||
|
downloadResolve1(download)
|
||||||
|
}
|
||||||
|
downloadCnt++
|
||||||
|
})
|
||||||
|
await page.getByRole('button', { name: 'Submit command' }).click()
|
||||||
|
|
||||||
|
// Handle download
|
||||||
|
const download = await downloadPromise1
|
||||||
|
const downloadLocationer = (extra = '', isImage = false) =>
|
||||||
|
`./e2e/playwright/export-snapshots/${output.type}-${
|
||||||
|
'storage' in output ? output.storage : ''
|
||||||
|
}${extra}.${isImage ? 'png' : output.type}`
|
||||||
|
const downloadLocation = downloadLocationer()
|
||||||
|
|
||||||
|
await download.saveAs(downloadLocation)
|
||||||
|
|
||||||
|
if (output.type === 'step') {
|
||||||
|
// stable timestamps for step files
|
||||||
|
const fileContents = await fsp.readFile(downloadLocation, 'utf-8')
|
||||||
|
const newFileContents = fileContents.replace(
|
||||||
|
/[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\.[0-9]+[0-9]+[0-9]\+[0-9]{2}:[0-9]{2}/g,
|
||||||
|
'1970-01-01T00:00:00.0+00:00'
|
||||||
|
)
|
||||||
|
await fsp.writeFile(downloadLocation, newFileContents)
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
modelPath: downloadLocation,
|
||||||
|
imagePath: downloadLocationer('', true),
|
||||||
|
outputType: output.type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -18,7 +18,7 @@ export default defineConfig({
|
|||||||
/* Retry on CI only */
|
/* Retry on CI only */
|
||||||
retries: process.env.CI ? 3 : 0,
|
retries: process.env.CI ? 3 : 0,
|
||||||
/* Opt out of parallel tests on CI. */
|
/* Opt out of parallel tests on CI. */
|
||||||
workers: process.env.CI ? 2 : 1,
|
workers: process.env.CI ? 1 : 1,
|
||||||
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
|
||||||
reporter: 'html',
|
reporter: 'html',
|
||||||
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
|
||||||
|
|||||||
@ -34,6 +34,7 @@ import {
|
|||||||
handleSelectionBatch,
|
handleSelectionBatch,
|
||||||
isSelectionLastLine,
|
isSelectionLastLine,
|
||||||
isSketchPipe,
|
isSketchPipe,
|
||||||
|
updateSelections,
|
||||||
} from 'lib/selections'
|
} from 'lib/selections'
|
||||||
import { applyConstraintIntersect } from './Toolbar/Intersect'
|
import { applyConstraintIntersect } from './Toolbar/Intersect'
|
||||||
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
|
import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance'
|
||||||
@ -53,6 +54,7 @@ import {
|
|||||||
} from 'lang/modifyAst'
|
} from 'lang/modifyAst'
|
||||||
import {
|
import {
|
||||||
Program,
|
Program,
|
||||||
|
Value,
|
||||||
VariableDeclaration,
|
VariableDeclaration,
|
||||||
coreDump,
|
coreDump,
|
||||||
parse,
|
parse,
|
||||||
@ -73,10 +75,7 @@ import { useSearchParams } from 'react-router-dom'
|
|||||||
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
|
import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls'
|
||||||
import { getVarNameModal } from 'hooks/useToolbarGuards'
|
import { getVarNameModal } from 'hooks/useToolbarGuards'
|
||||||
import useHotkeyWrapper from 'lib/hotkeyWrapper'
|
import useHotkeyWrapper from 'lib/hotkeyWrapper'
|
||||||
import {
|
import { applyConstraintEqualAngle } from './Toolbar/EqualAngle'
|
||||||
EngineConnectionState,
|
|
||||||
EngineConnectionStateType,
|
|
||||||
} from 'lang/std/engineConnection'
|
|
||||||
|
|
||||||
type MachineContext<T extends AnyStateMachine> = {
|
type MachineContext<T extends AnyStateMachine> = {
|
||||||
state: StateFrom<T>
|
state: StateFrom<T>
|
||||||
@ -223,8 +222,7 @@ export const ModelingMachineProvider = ({
|
|||||||
: {}
|
: {}
|
||||||
),
|
),
|
||||||
'Set selection': assign(({ selectionRanges }, event) => {
|
'Set selection': assign(({ selectionRanges }, event) => {
|
||||||
if (event.type !== 'Set selection') return {} // this was needed for ts after adding 'Set selection' action to on done modal events
|
const setSelections = event.data as SetSelections // this was needed for ts after adding 'Set selection' action to on done modal events
|
||||||
const setSelections = event.data
|
|
||||||
if (!editorManager.editorView) return {}
|
if (!editorManager.editorView) return {}
|
||||||
const dispatchSelection = (selection?: EditorSelection) => {
|
const dispatchSelection = (selection?: EditorSelection) => {
|
||||||
if (!selection) return // TODO less of hack for the below please
|
if (!selection) return // TODO less of hack for the below please
|
||||||
@ -311,11 +309,18 @@ export const ModelingMachineProvider = ({
|
|||||||
selectionRanges: selections,
|
selectionRanges: selections,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (setSelections.selectionType === 'completeSelection') {
|
||||||
|
editorManager.selectRange(setSelections.selection)
|
||||||
|
return {
|
||||||
|
selectionRanges: setSelections.selection,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
}),
|
}),
|
||||||
'Engine export': (_, event) => {
|
'Engine export': async (_, event) => {
|
||||||
if (event.type !== 'Export' || TEST) return
|
if (event.type !== 'Export' || TEST) return
|
||||||
|
console.log('exporting', event.data)
|
||||||
const format = {
|
const format = {
|
||||||
...event.data,
|
...event.data,
|
||||||
} as Partial<Models['OutputFormat_type']>
|
} as Partial<Models['OutputFormat_type']>
|
||||||
@ -359,9 +364,16 @@ export const ModelingMachineProvider = ({
|
|||||||
format.selection = { type: 'default_scene' }
|
format.selection = { type: 'default_scene' }
|
||||||
}
|
}
|
||||||
|
|
||||||
exportFromEngine({
|
toast.promise(
|
||||||
format: format as Models['OutputFormat_type'],
|
exportFromEngine({
|
||||||
}).catch((e) => toast.error('Error while exporting', e)) // TODO I think we need to throw the error from engineCommandManager
|
format: format as Models['OutputFormat_type'],
|
||||||
|
}),
|
||||||
|
{
|
||||||
|
loading: 'Exporting...',
|
||||||
|
success: 'Exported successfully',
|
||||||
|
error: 'Error while exporting',
|
||||||
|
}
|
||||||
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
guards: {
|
guards: {
|
||||||
|
|||||||
@ -144,7 +144,7 @@ export async function applyConstraintAngleLength({
|
|||||||
pathToNodeMap,
|
pathToNodeMap,
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log('erorr', e)
|
console.log('error', e)
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -994,6 +994,10 @@ export class EngineCommandManager {
|
|||||||
engineConnection?: EngineConnection
|
engineConnection?: EngineConnection
|
||||||
defaultPlanes: DefaultPlanes | null = null
|
defaultPlanes: DefaultPlanes | null = null
|
||||||
commandLogs: CommandLog[] = []
|
commandLogs: CommandLog[] = []
|
||||||
|
pendingExport?: {
|
||||||
|
resolve: (filename?: string) => void
|
||||||
|
reject: (reason: any) => void
|
||||||
|
}
|
||||||
_commandLogCallBack: (command: CommandLog[]) => void = () => {}
|
_commandLogCallBack: (command: CommandLog[]) => void = () => {}
|
||||||
private resolveReady = () => {}
|
private resolveReady = () => {}
|
||||||
/** Folks should realize that wait for ready does not get called _everytime_
|
/** Folks should realize that wait for ready does not get called _everytime_
|
||||||
@ -1150,7 +1154,9 @@ export class EngineCommandManager {
|
|||||||
// because in all other cases we send JSON strings. But in the case of
|
// because in all other cases we send JSON strings. But in the case of
|
||||||
// export we send a binary blob.
|
// export we send a binary blob.
|
||||||
// Pass this to our export function.
|
// Pass this to our export function.
|
||||||
void exportSave(event.data)
|
exportSave(event.data).then(() => {
|
||||||
|
this.pendingExport?.resolve()
|
||||||
|
}, this.pendingExport?.reject)
|
||||||
} else {
|
} else {
|
||||||
const message: Models['WebSocketResponse_type'] = JSON.parse(
|
const message: Models['WebSocketResponse_type'] = JSON.parse(
|
||||||
event.data
|
event.data
|
||||||
@ -1548,6 +1554,12 @@ export class EngineCommandManager {
|
|||||||
this.outSequence++
|
this.outSequence++
|
||||||
this.engineConnection?.unreliableSend(command)
|
this.engineConnection?.unreliableSend(command)
|
||||||
return Promise.resolve()
|
return Promise.resolve()
|
||||||
|
} else if (cmd.type === 'export') {
|
||||||
|
const promise = new Promise((resolve, reject) => {
|
||||||
|
this.pendingExport = { resolve, reject }
|
||||||
|
})
|
||||||
|
this.engineConnection?.send(command)
|
||||||
|
return promise
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
command.cmd.type === 'default_camera_look_at' ||
|
command.cmd.type === 'default_camera_look_at' ||
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import {
|
|||||||
kclManager,
|
kclManager,
|
||||||
sceneEntitiesManager,
|
sceneEntitiesManager,
|
||||||
} from 'lib/singletons'
|
} from 'lib/singletons'
|
||||||
import { CallExpression, SourceRange, parse, recast } from 'lang/wasm'
|
import { CallExpression, SourceRange, Value, parse, recast } from 'lang/wasm'
|
||||||
import { ModelingMachineEvent } from 'machines/modelingMachine'
|
import { ModelingMachineEvent } from 'machines/modelingMachine'
|
||||||
import { uuidv4 } from 'lib/utils'
|
import { uuidv4 } from 'lib/utils'
|
||||||
import { EditorSelection } from '@codemirror/state'
|
import { EditorSelection } from '@codemirror/state'
|
||||||
@ -27,6 +27,7 @@ import {
|
|||||||
} from 'clientSideScene/sceneEntities'
|
} from 'clientSideScene/sceneEntities'
|
||||||
import { Mesh, Object3D, Object3DEventMap } from 'three'
|
import { Mesh, Object3D, Object3DEventMap } from 'three'
|
||||||
import { AXIS_GROUP, X_AXIS } from 'clientSideScene/sceneInfra'
|
import { AXIS_GROUP, X_AXIS } from 'clientSideScene/sceneInfra'
|
||||||
|
import { PathToNodeMap } from 'lang/std/sketchcombos'
|
||||||
|
|
||||||
export const X_AXIS_UUID = 'ad792545-7fd3-482a-a602-a93924e3055b'
|
export const X_AXIS_UUID = 'ad792545-7fd3-482a-a602-a93924e3055b'
|
||||||
export const Y_AXIS_UUID = '680fd157-266f-4b8a-984f-cdf46b8bdf01'
|
export const Y_AXIS_UUID = '680fd157-266f-4b8a-984f-cdf46b8bdf01'
|
||||||
@ -564,3 +565,22 @@ export function sendSelectEventToEngine(
|
|||||||
.then((res) => res.data.data)
|
.then((res) => res.data.data)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function updateSelections(
|
||||||
|
pathToNodeMap: PathToNodeMap,
|
||||||
|
prevSelectionRanges: Selections,
|
||||||
|
ast: Program
|
||||||
|
): Selections {
|
||||||
|
return {
|
||||||
|
...prevSelectionRanges,
|
||||||
|
codeBasedSelections: Object.entries(pathToNodeMap).map(
|
||||||
|
([index, pathToNode]): Selection => {
|
||||||
|
const node = getNodeFromPath<Value>(ast, pathToNode).node
|
||||||
|
return {
|
||||||
|
range: [node.start, node.end],
|
||||||
|
type: prevSelectionRanges.codeBasedSelections[Number(index)]?.type,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user