Merge branch 'main' into coredump-clientstate
This commit is contained in:
		| @ -1,5 +1,5 @@ | ||||
| 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 { roundOff, uuidv4 } from 'lib/utils' | ||||
| 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('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' }) | ||||
|   ).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 { getUtils } from './test-utils' | ||||
| import { Paths, doExport, getUtils } from './test-utils' | ||||
| import { Models } from '@kittycad/lib' | ||||
| import fsp from 'fs/promises' | ||||
| 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 path from 'path' | ||||
| import { TEST_SETTINGS, TEST_SETTINGS_KEY } from './storageStates' | ||||
| @ -99,78 +99,6 @@ const part001 = startSketchOn('-XZ') | ||||
|   await page.waitForTimeout(1000) | ||||
|   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'] = { | ||||
|     axis: 'z', | ||||
|     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 | ||||
|   // the rest are only there to make typescript happy | ||||
|   exportLocations.push( | ||||
|     await doExport({ | ||||
|       type: 'step', | ||||
|       coords: sysType, | ||||
|     }) | ||||
|     await doExport( | ||||
|       { | ||||
|         type: 'step', | ||||
|         coords: sysType, | ||||
|       }, | ||||
|       page | ||||
|     ) | ||||
|   ) | ||||
|   exportLocations.push( | ||||
|     await doExport({ | ||||
|       type: 'ply', | ||||
|       coords: sysType, | ||||
|       selection: { type: 'default_scene' }, | ||||
|       storage: 'ascii', | ||||
|       units: 'in', | ||||
|     }) | ||||
|     await doExport( | ||||
|       { | ||||
|         type: 'ply', | ||||
|         coords: sysType, | ||||
|         selection: { type: 'default_scene' }, | ||||
|         storage: 'ascii', | ||||
|         units: 'in', | ||||
|       }, | ||||
|       page | ||||
|     ) | ||||
|   ) | ||||
|   exportLocations.push( | ||||
|     await doExport({ | ||||
|       type: 'ply', | ||||
|       storage: 'binary_little_endian', | ||||
|       coords: sysType, | ||||
|       selection: { type: 'default_scene' }, | ||||
|       units: 'in', | ||||
|     }) | ||||
|     await doExport( | ||||
|       { | ||||
|         type: 'ply', | ||||
|         storage: 'binary_little_endian', | ||||
|         coords: sysType, | ||||
|         selection: { type: 'default_scene' }, | ||||
|         units: 'in', | ||||
|       }, | ||||
|       page | ||||
|     ) | ||||
|   ) | ||||
|   exportLocations.push( | ||||
|     await doExport({ | ||||
|       type: 'ply', | ||||
|       storage: 'binary_big_endian', | ||||
|       coords: sysType, | ||||
|       selection: { type: 'default_scene' }, | ||||
|       units: 'in', | ||||
|     }) | ||||
|     await doExport( | ||||
|       { | ||||
|         type: 'ply', | ||||
|         storage: 'binary_big_endian', | ||||
|         coords: sysType, | ||||
|         selection: { type: 'default_scene' }, | ||||
|         units: 'in', | ||||
|       }, | ||||
|       page | ||||
|     ) | ||||
|   ) | ||||
|   exportLocations.push( | ||||
|     await doExport({ | ||||
|       type: 'stl', | ||||
|       storage: 'ascii', | ||||
|       coords: sysType, | ||||
|       units: 'in', | ||||
|       selection: { type: 'default_scene' }, | ||||
|     }) | ||||
|     await doExport( | ||||
|       { | ||||
|         type: 'stl', | ||||
|         storage: 'ascii', | ||||
|         coords: sysType, | ||||
|         units: 'in', | ||||
|         selection: { type: 'default_scene' }, | ||||
|       }, | ||||
|       page | ||||
|     ) | ||||
|   ) | ||||
|   exportLocations.push( | ||||
|     await doExport({ | ||||
|       type: 'stl', | ||||
|       storage: 'binary', | ||||
|       coords: sysType, | ||||
|       units: 'in', | ||||
|       selection: { type: 'default_scene' }, | ||||
|     }) | ||||
|     await doExport( | ||||
|       { | ||||
|         type: 'stl', | ||||
|         storage: 'binary', | ||||
|         coords: sysType, | ||||
|         units: 'in', | ||||
|         selection: { type: 'default_scene' }, | ||||
|       }, | ||||
|       page | ||||
|     ) | ||||
|   ) | ||||
|   exportLocations.push( | ||||
|     await doExport({ | ||||
|       // obj seems to be a little flaky, times out tests sometimes | ||||
|       type: 'obj', | ||||
|       coords: sysType, | ||||
|       units: 'in', | ||||
|     }) | ||||
|     await doExport( | ||||
|       { | ||||
|         // obj seems to be a little flaky, times out tests sometimes | ||||
|         type: 'obj', | ||||
|         coords: sysType, | ||||
|         units: 'in', | ||||
|       }, | ||||
|       page | ||||
|     ) | ||||
|   ) | ||||
|   exportLocations.push( | ||||
|     await doExport({ | ||||
|       type: 'gltf', | ||||
|       storage: 'embedded', | ||||
|       presentation: 'pretty', | ||||
|     }) | ||||
|     await doExport( | ||||
|       { | ||||
|         type: 'gltf', | ||||
|         storage: 'embedded', | ||||
|         presentation: 'pretty', | ||||
|       }, | ||||
|       page | ||||
|     ) | ||||
|   ) | ||||
|   exportLocations.push( | ||||
|     await doExport({ | ||||
|       type: 'gltf', | ||||
|       storage: 'binary', | ||||
|       presentation: 'pretty', | ||||
|     }) | ||||
|     await doExport( | ||||
|       { | ||||
|         type: 'gltf', | ||||
|         storage: 'binary', | ||||
|         presentation: 'pretty', | ||||
|       }, | ||||
|       page | ||||
|     ) | ||||
|   ) | ||||
|   exportLocations.push( | ||||
|     await doExport({ | ||||
|       type: 'gltf', | ||||
|       storage: 'standard', | ||||
|       presentation: 'pretty', | ||||
|     }) | ||||
|     await doExport( | ||||
|       { | ||||
|         type: 'gltf', | ||||
|         storage: 'standard', | ||||
|         presentation: 'pretty', | ||||
|       }, | ||||
|       page | ||||
|     ) | ||||
|   ) | ||||
|  | ||||
|   // 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 fsp from 'fs/promises' | ||||
| import pixelMatch from 'pixelmatch' | ||||
| import { PNG } from 'pngjs' | ||||
| import { Protocol } from 'playwright-core/types/protocol' | ||||
| import type { Models } from '@kittycad/lib' | ||||
| import { APP_NAME } from 'lib/constants' | ||||
|  | ||||
| async function waitForPageLoad(page: Page) { | ||||
|   // 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 */ | ||||
|   retries: process.env.CI ? 3 : 0, | ||||
|   /* 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: 'html', | ||||
|   /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ | ||||
|  | ||||
| @ -34,6 +34,7 @@ import { | ||||
|   handleSelectionBatch, | ||||
|   isSelectionLastLine, | ||||
|   isSketchPipe, | ||||
|   updateSelections, | ||||
| } from 'lib/selections' | ||||
| import { applyConstraintIntersect } from './Toolbar/Intersect' | ||||
| import { applyConstraintAbsDistance } from './Toolbar/SetAbsDistance' | ||||
| @ -53,6 +54,7 @@ import { | ||||
| } from 'lang/modifyAst' | ||||
| import { | ||||
|   Program, | ||||
|   Value, | ||||
|   VariableDeclaration, | ||||
|   coreDump, | ||||
|   parse, | ||||
| @ -73,10 +75,7 @@ import { useSearchParams } from 'react-router-dom' | ||||
| import { letEngineAnimateAndSyncCamAfter } from 'clientSideScene/CameraControls' | ||||
| import { getVarNameModal } from 'hooks/useToolbarGuards' | ||||
| import useHotkeyWrapper from 'lib/hotkeyWrapper' | ||||
| import { | ||||
|   EngineConnectionState, | ||||
|   EngineConnectionStateType, | ||||
| } from 'lang/std/engineConnection' | ||||
| import { applyConstraintEqualAngle } from './Toolbar/EqualAngle' | ||||
|  | ||||
| type MachineContext<T extends AnyStateMachine> = { | ||||
|   state: StateFrom<T> | ||||
| @ -223,8 +222,7 @@ export const ModelingMachineProvider = ({ | ||||
|             : {} | ||||
|         ), | ||||
|         '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 | ||||
|           const setSelections = event.data as SetSelections // this was needed for ts after adding 'Set selection' action to on done modal events | ||||
|           if (!editorManager.editorView) return {} | ||||
|           const dispatchSelection = (selection?: EditorSelection) => { | ||||
|             if (!selection) return // TODO less of hack for the below please | ||||
| @ -311,11 +309,18 @@ export const ModelingMachineProvider = ({ | ||||
|               selectionRanges: selections, | ||||
|             } | ||||
|           } | ||||
|           if (setSelections.selectionType === 'completeSelection') { | ||||
|             editorManager.selectRange(setSelections.selection) | ||||
|             return { | ||||
|               selectionRanges: setSelections.selection, | ||||
|             } | ||||
|           } | ||||
|  | ||||
|           return {} | ||||
|         }), | ||||
|         'Engine export': (_, event) => { | ||||
|         'Engine export': async (_, event) => { | ||||
|           if (event.type !== 'Export' || TEST) return | ||||
|           console.log('exporting', event.data) | ||||
|           const format = { | ||||
|             ...event.data, | ||||
|           } as Partial<Models['OutputFormat_type']> | ||||
| @ -359,9 +364,16 @@ export const ModelingMachineProvider = ({ | ||||
|             format.selection = { type: 'default_scene' } | ||||
|           } | ||||
|  | ||||
|           exportFromEngine({ | ||||
|             format: format as Models['OutputFormat_type'], | ||||
|           }).catch((e) => toast.error('Error while exporting', e)) // TODO I think we need to throw the error from engineCommandManager | ||||
|           toast.promise( | ||||
|             exportFromEngine({ | ||||
|               format: format as Models['OutputFormat_type'], | ||||
|             }), | ||||
|             { | ||||
|               loading: 'Exporting...', | ||||
|               success: 'Exported successfully', | ||||
|               error: 'Error while exporting', | ||||
|             } | ||||
|           ) | ||||
|         }, | ||||
|       }, | ||||
|       guards: { | ||||
|  | ||||
| @ -144,7 +144,7 @@ export async function applyConstraintAngleLength({ | ||||
|       pathToNodeMap, | ||||
|     } | ||||
|   } catch (e) { | ||||
|     console.log('erorr', e) | ||||
|     console.log('error', e) | ||||
|     throw e | ||||
|   } | ||||
| } | ||||
|  | ||||
| @ -994,6 +994,10 @@ export class EngineCommandManager { | ||||
|   engineConnection?: EngineConnection | ||||
|   defaultPlanes: DefaultPlanes | null = null | ||||
|   commandLogs: CommandLog[] = [] | ||||
|   pendingExport?: { | ||||
|     resolve: (filename?: string) => void | ||||
|     reject: (reason: any) => void | ||||
|   } | ||||
|   _commandLogCallBack: (command: CommandLog[]) => void = () => {} | ||||
|   private resolveReady = () => {} | ||||
|   /** 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 | ||||
|             // export we send a binary blob. | ||||
|             // Pass this to our export function. | ||||
|             void exportSave(event.data) | ||||
|             exportSave(event.data).then(() => { | ||||
|               this.pendingExport?.resolve() | ||||
|             }, this.pendingExport?.reject) | ||||
|           } else { | ||||
|             const message: Models['WebSocketResponse_type'] = JSON.parse( | ||||
|               event.data | ||||
| @ -1548,6 +1554,12 @@ export class EngineCommandManager { | ||||
|       this.outSequence++ | ||||
|       this.engineConnection?.unreliableSend(command) | ||||
|       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 ( | ||||
|       command.cmd.type === 'default_camera_look_at' || | ||||
|  | ||||
| @ -5,7 +5,7 @@ import { | ||||
|   kclManager, | ||||
|   sceneEntitiesManager, | ||||
| } 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 { uuidv4 } from 'lib/utils' | ||||
| import { EditorSelection } from '@codemirror/state' | ||||
| @ -27,6 +27,7 @@ import { | ||||
| } from 'clientSideScene/sceneEntities' | ||||
| import { Mesh, Object3D, Object3DEventMap } from 'three' | ||||
| 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 Y_AXIS_UUID = '680fd157-266f-4b8a-984f-cdf46b8bdf01' | ||||
| @ -564,3 +565,22 @@ export function sendSelectEventToEngine( | ||||
|     .then((res) => res.data.data) | ||||
|   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