Compare commits

..

7 Commits

Author SHA1 Message Date
e1da72a0ae Update common points to reflect empty zoom-to-fit 2024-08-05 17:40:06 +02:00
ec2d1999a7 fmt 2024-08-05 16:31:07 +02:00
95683f1cc1 A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) 2024-08-05 14:17:29 +00:00
f48f1c21c1 A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu) 2024-08-05 14:16:05 +00:00
5cdf2de89a Reset camera position when artifact graph is cleared 2024-08-05 16:08:28 +02:00
543e809739 Add "report a bug" mention to user menu onboarding step (#3278)
* Mention "report a bug" in user menu onboarding step

* Add to a test so we match QAWolf a bit more

* Re-run CI
2024-08-05 08:24:19 -04:00
61b669cf4e github actions Playwright shard execution (#3199)
* add @snapshot tag to all snapshot-tests

* set workers to 1 on CI

* try sharding on google chrome only

* add retry when navigating to the app (on CI)

* reduce ubuntu-cores to 2

* revert runner back to 8 cores

* re-add retry + enable macos

* Reduce timeouts to 30 minutes

* ensure retry is triggered

* removed sharding when retrying failures

* added --pass-with-no-tests

* ensure failure occurs

* revert failure

* use smaller sized runners

* revert back to supported runner size

* revert to og version

* minimize changes

* yarn fmt

* ensure failure

* undo failure

---------

Co-authored-by: ryanrosello-og <ry@zoo.dev>
2024-08-05 21:30:16 +10:00
15 changed files with 682 additions and 805 deletions

View File

@ -13,7 +13,7 @@ permissions:
contents: write
pull-requests: write
actions: read
jobs:
@ -34,8 +34,13 @@ jobs:
- 'src/wasm-lib/**'
playwright-ubuntu:
timeout-minutes: 60
timeout-minutes: 30
runs-on: ubuntu-latest-8-cores
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4]
shardTotal: [4]
needs: check-rust-changes
steps:
- name: Tune GitHub-hosted runner network
@ -107,7 +112,7 @@ jobs:
run: yarn build:local
- name: Run ubuntu/chrome snapshots
run: |
yarn playwright test --project="Google Chrome" --retries="3" --update-snapshots e2e/playwright/snapshot-tests.spec.ts
yarn playwright test --project="Google Chrome" --retries="3" --update-snapshots --grep=@snapshot --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }}
env:
CI: true
token: ${{ secrets.KITTYCAD_API_TOKEN_DEV }}
@ -115,7 +120,7 @@ jobs:
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-ubuntu-snapshot-${{ github.sha }}
name: playwright-report-ubuntu-snapshot-${{ matrix.shardIndex }}-${{ github.sha }}
path: playwright-report/
retention-days: 30
overwrite: true
@ -149,7 +154,7 @@ jobs:
- uses: actions/upload-artifact@v4
if: steps.git-check.outputs.modified == 'true'
with:
name: playwright-report-ubuntu-${{ github.sha }}
name: playwright-report-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }}
path: playwright-report/
retention-days: 30
# if have previous run results, use them
@ -157,7 +162,7 @@ jobs:
if: always()
continue-on-error: true
with:
name: test-results-ubuntu-${{ github.sha }}
name: test-results-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }}
path: test-results/
- name: Run ubuntu/chrome flow (with retries)
id: retry
@ -166,14 +171,14 @@ jobs:
if [[ ! -f "test-results/.last-run.json" ]]; then
# if no last run artifact, than run plawright normally
echo "run playwright normally"
yarn playwright test --project="Google Chrome" e2e/playwright/flow-tests.spec.ts || true
yarn playwright test --project="Google Chrome" --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --grep-invert=@snapshot || true
# # send to axiom
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
fi
retry=1
max_retrys=4
# retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues
while [[ $retry -le $max_retrys ]]; do
if [[ -f "test-results/.last-run.json" ]]; then
@ -181,7 +186,7 @@ jobs:
if [[ $failed_tests -gt 0 ]]; then
echo "retried=true" >>$GITHUB_OUTPUT
echo "run playwright with last failed tests and retry $retry"
yarn playwright test --project="Google Chrome" --last-failed e2e/playwright/flow-tests.spec.ts || true
yarn playwright test --project="Google Chrome" --last-failed --grep-invert=@snapshot || true
# send to axiom
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
retry=$((retry + 1))
@ -194,9 +199,9 @@ jobs:
exit 0
fi
done
echo "retried=false" >>$GITHUB_OUTPUT
if [[ -f "test-results/.last-run.json" ]]; then
failed_tests=$(jq '.failedTests | length' test-results/.last-run.json)
if [[ $failed_tests -gt 0 ]]; then
@ -216,21 +221,26 @@ jobs:
- uses: actions/upload-artifact@v4
if: always()
with:
name: test-results-ubuntu-${{ github.sha }}
name: test-results-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }}
path: test-results/
retention-days: 30
overwrite: true
- uses: actions/upload-artifact@v4
if: always()
with:
name: playwright-report-ubuntu-${{ github.sha }}
name: playwright-report-ubuntu-${{ matrix.shardIndex }}-${{ github.sha }}
path: playwright-report/
retention-days: 30
overwrite: true
playwright-macos:
timeout-minutes: 60
runs-on: macos-14-large
timeout-minutes: 30
runs-on: macos-14
strategy:
fail-fast: false
matrix:
shardIndex: [1, 2, 3, 4]
shardTotal: [4]
needs: check-rust-changes
steps:
- name: Tune GitHub-hosted runner network
@ -306,7 +316,7 @@ jobs:
if: ${{ always() }}
continue-on-error: true
with:
name: test-results-macos-${{ github.sha }}
name: test-results-macos-${{ matrix.shardIndex }}-${{ github.sha }}
path: test-results/
- name: Run macos/safari flow (with retries)
id: retry
@ -315,14 +325,14 @@ jobs:
if [[ ! -f "test-results/.last-run.json" ]]; then
# if no last run artifact, than run plawright normally
echo "run playwright normally"
yarn playwright test --project="webkit" e2e/playwright/flow-tests.spec.ts || true
yarn playwright test --project="webkit" --shard=${{ matrix.shardIndex }}/${{ matrix.shardTotal }} --grep-invert=@snapshot || true
# # send to axiom
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
fi
retry=1
max_retrys=4
# retry failed tests, doing our own retries because using inbuilt playwright retries causes connection issues
while [[ $retry -le $max_retrys ]]; do
if [[ -f "test-results/.last-run.json" ]]; then
@ -330,7 +340,7 @@ jobs:
if [[ $failed_tests -gt 0 ]]; then
echo "retried=true" >>$GITHUB_OUTPUT
echo "run playwright with last failed tests and retry $retry"
yarn playwright test --project="webkit" --last-failed e2e/playwright/flow-tests.spec.ts || true
yarn playwright test --project="webkit" --last-failed --grep-invert=@snapshot || true
# send to axiom
node playwrightProcess.mjs | tee /tmp/github-actions.log > /dev/null 2>&1
retry=$((retry + 1))
@ -343,9 +353,9 @@ jobs:
exit 0
fi
done
echo "retried=false" >>$GITHUB_OUTPUT
if [[ -f "test-results/.last-run.json" ]]; then
failed_tests=$(jq '.failedTests | length' test-results/.last-run.json)
if [[ $failed_tests -gt 0 ]]; then
@ -360,15 +370,14 @@ jobs:
- uses: actions/upload-artifact@v4
if: ${{ always() }}
with:
name: test-results-macos-${{ github.sha }}
name: test-results-macos-${{ matrix.shardIndex }}-${{ github.sha }}
path: test-results/
retention-days: 30
overwrite: true
- uses: actions/upload-artifact@v4
if: ${{ always() }}
with:
name: playwright-report-macos-${{ github.sha }}
name: playwright-report-macos-${{ matrix.shardIndex }}-${{ github.sha }}
path: playwright-report/
retention-days: 30
overwrite: true

View File

@ -46,9 +46,9 @@ document.addEventListener('mousemove', (e) =>
const deg = (Math.PI * 2) / 360
const commonPoints = {
startAt: '[7.19, -9.7]',
num1: 7.25,
num2: 14.44,
startAt: '[0.75, -1.01]',
num1: 0.75,
num2: 1.5,
}
test.afterEach(async ({ context, page }, testInfo) => {
@ -2535,18 +2535,29 @@ test.describe('Onboarding tests', () => {
await page.waitForURL('**/file/**', { waitUntil: 'domcontentloaded' })
// Test that the text in this step is correct
const avatarLocator = await page
.getByTestId('user-sidebar-toggle')
.locator('img')
const onboardingOverlayLocator = await page
const sidebar = page.getByTestId('user-sidebar-toggle')
const avatar = sidebar.locator('img')
const onboardingOverlayLocator = page
.getByTestId('onboarding-content')
.locator('div')
.nth(1)
// Expect the avatar to be visible and for the text to reference it
await expect(avatarLocator).not.toBeVisible()
await expect(avatar).not.toBeVisible()
await expect(onboardingOverlayLocator).toBeVisible()
await expect(onboardingOverlayLocator).toContainText('the menu button')
// Test we mention what else is in this menu for https://github.com/KittyCAD/modeling-app/issues/2939
// which doesn't deserver its own full test spun up
const userMenuFeatures = [
'manage your account',
'report a bug',
'request a feature',
'sign out',
]
for (const feature of userMenuFeatures) {
await expect(onboardingOverlayLocator).toContainText(feature)
}
})
})

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 37 KiB

After

Width:  |  Height:  |  Size: 29 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 31 KiB

After

Width:  |  Height:  |  Size: 29 KiB

View File

@ -15,6 +15,23 @@ export const TEST_COLORS = {
BLUE: [0, 0, 255] as TestColor,
} as const
async function waitForPageLoadWithRetry(page: Page) {
await expect(async () => {
await page.goto('/')
const errorMessage = 'App failed to load - 🔃 Retrying ...'
await expect(page.getByTestId('loading'), errorMessage).not.toBeAttached({
timeout: 20_000,
})
await expect(
page.getByRole('button', { name: 'Start Sketch' }),
errorMessage
).toBeEnabled({
timeout: 20_000,
})
}).toPass({ timeout: 70_000, intervals: [1_000] })
}
async function waitForPageLoad(page: Page) {
// wait for all spinners to be gone
await expect(page.getByTestId('loading')).not.toBeAttached({
@ -218,9 +235,12 @@ async function waitForAuthAndLsp(page: Page) {
}
return false
})
await page.goto('/')
await waitForPageLoad(page)
if (process.env.CI) {
await waitForPageLoadWithRetry(page)
} else {
await page.goto('/')
await waitForPageLoad(page)
}
return waitForLspPromise
}
@ -234,6 +254,7 @@ export async function getUtils(page: Page) {
return {
waitForAuthSkipAppStart: () => waitForAuthAndLsp(page),
waitForPageLoad: () => waitForPageLoad(page),
waitForPageLoadWithRetry: () => waitForPageLoadWithRetry(page),
removeCurrentCode: () => removeCurrentCode(page),
sendCustomCmd: (cmd: EngineCommand) => sendCustomCmd(page, cmd),
updateCamPosition: async (xyz: [number, number, number]) => {

View File

@ -18,7 +18,7 @@ export default defineConfig({
/* Do not retry */
retries: process.env.CI ? 0 : 0,
/* Different amount of parallelism on CI and local. */
workers: process.env.CI ? 4 : 4,
workers: process.env.CI ? 1 : 4,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: [
[process.env.CI ? 'dot' : 'list'],

View File

@ -98,7 +98,6 @@ import {
import { getThemeColorForThreeJs } from 'lib/theme'
import { err, trap } from 'lib/trap'
import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer'
import { addCircleToSketchAst } from 'lib/circleTool'
type DraftSegment = 'line' | 'tangentialArcTo'
@ -715,8 +714,11 @@ export class SceneEntities {
})
}
setupDraftRectangle = async (
rectangleOrigin: [number, number],
{ sketchPathToNode, origin, zAxis, yAxis }: SketchDetails
sketchPathToNode: PathToNode,
forward: [number, number, number],
up: [number, number, number],
sketchOrigin: [number, number, number],
rectangleOrigin: [x: number, y: number]
) => {
let _ast = structuredClone(kclManager.ast)
@ -748,9 +750,9 @@ export class SceneEntities {
const { programMemoryOverride, truncatedAst } = await this.setupSketch({
sketchPathToNode,
forward: yAxis,
up: zAxis,
position: origin,
forward,
up,
position: sketchOrigin,
maybeModdedAst: _ast,
draftExpressionsIndices: { start: 0, end: 3 },
})
@ -860,52 +862,6 @@ export class SceneEntities {
},
})
}
setupCircleOriginListener = () => {
sceneInfra.setCallbacks({
onClick: (args) => {
const twoD = args.intersectionPoint?.twoD
if (!twoD) {
console.warn(`This click didn't have a 2D intersection`, args)
return
}
sceneInfra.modelingSend({
type: 'Add circle origin',
data: [twoD.x, twoD.y],
})
},
})
}
setupDraftCircle = async (
circleOrigin: [number, number],
{ sketchPathToNode, origin, zAxis, yAxis }: SketchDetails
) => {
const astWithCircle = await addCircleToSketchAst({
sourceAst: kclManager.ast,
sketchPathToNode,
circleOrigin,
})
const { programMemoryOverride, truncatedAst } = await this.setupSketch({
sketchPathToNode,
forward: yAxis,
up: zAxis,
position: origin,
maybeModdedAst: astWithCircle,
draftExpressionsIndices: { start: 0, end: 3 },
})
// TODO: Move this into the onclick handler to get the real shit
// Update the primary AST and unequip the rectangle tool
await kclManager.executeAstMock(astWithCircle)
sceneInfra.modelingSend({ type: 'CancelSketch' })
const { programMemory } = await executeAst({
ast: astWithCircle,
useFakeExecutor: true,
engineCommandManager: this.engineCommandManager,
programMemoryOverride,
})
}
setupSketchIdleCallbacks = ({
pathToNode,
up,
@ -1220,9 +1176,6 @@ export class SceneEntities {
? orthoFactor
: perspScale(sceneInfra.camControls.camera, group)) /
sceneInfra._baseUnitMultiplier
console.log('segment type', type)
if (type === TANGENTIAL_ARC_TO_SEGMENT) {
return this.updateTangentialArcToSegment({
prevSegment: sgPaths[index - 1],

View File

@ -1,92 +0,0 @@
import {
createArrayExpression,
createCallExpressionStdLib,
createLiteral,
createPipeExpression,
createPipeSubstitution,
createTagDeclarator,
findUniqueName,
} from 'lang/modifyAst'
import { roundOff } from './utils'
import {
PathToNode,
Program,
VariableDeclaration,
parse,
recast,
} from 'lang/wasm'
import { getNodeFromPath } from 'lang/queryAst'
import { trap } from './trap'
/**
* Hide away the working with the AST
*/
export async function addCircleToSketchAst({
sourceAst,
sketchPathToNode,
circleOrigin,
}: {
sourceAst: Program
sketchPathToNode?: PathToNode
circleOrigin?: [number, number]
}) {
let _ast = JSON.parse(JSON.stringify(sourceAst))
const _node1 = getNodeFromPath<VariableDeclaration>(
_ast,
sketchPathToNode || [],
'VariableDeclaration'
)
if (trap(_node1)) return Promise.reject(_node1)
const tag = findUniqueName(_ast, 'circle')
const _node2 = getNodeFromPath<VariableDeclaration>(
_ast,
sketchPathToNode || [],
'VariableDeclaration'
)
if (trap(_node2)) return Promise.reject(_node2)
const startSketchOn = _node2.node?.declarations
const startSketchOnInit = startSketchOn?.[0]?.init
startSketchOn[0].init = createPipeExpression([
startSketchOnInit,
...getCircleCallExpressions({
center: circleOrigin,
tag,
}),
])
const maybeModdedAst = parse(recast(_ast))
if (trap(maybeModdedAst)) return Promise.reject(maybeModdedAst)
return Promise.resolve(maybeModdedAst)
}
/**
* Returns AST expressions for this KCL code:
* const yo = startSketchOn('XY')
* |> startProfileAt([0, 0], %)
* |> circle([0, 0], 0, %) <- this line
*/
export function getCircleCallExpressions({
center = [0, 0],
radius = 10,
tag,
}: {
center?: [number, number]
radius?: number
tag: string
}) {
return [
createCallExpressionStdLib('circle', [
createArrayExpression([
createLiteral(roundOff(center[0])),
createLiteral(roundOff(center[1])),
]),
createLiteral(roundOff(radius)),
createPipeSubstitution(),
createTagDeclarator(tag),
]),
]
}

View File

@ -293,8 +293,7 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
status: 'available',
disabled: (state) =>
state.matches('Sketch no face') ||
state.matches('Sketch.Rectangle tool.Awaiting second corner') ||
state.matches('Sketch.Circle tool.Awaiting perimeter click'),
state.matches('Sketch.Rectangle tool.Awaiting second corner'),
title: 'Line',
hotkey: (state) =>
state.matches('Sketch.Line tool') ? ['Esc', 'L'] : 'L',
@ -356,20 +355,9 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
[
{
id: 'circle-center',
onClick: ({ modelingStateMatches, modelingSend }) =>
modelingSend({
type: 'change tool',
data: {
tool: !modelingStateMatches('Sketch.Circle tool')
? 'circle'
: 'none',
},
}),
onClick: () => console.error('Center circle not yet implemented'),
icon: 'circle',
status: 'available',
disabled: (state) =>
!canRectangleTool(state.context) &&
!state.matches('Sketch.Circle tool'),
status: 'unavailable',
title: 'Center circle',
showTitle: false,
description: 'Start drawing a circle from its center',
@ -379,9 +367,6 @@ export const toolbarConfig: Record<ToolbarModeName, ToolbarMode> = {
url: 'https://github.com/KittyCAD/modeling-app/issues/1501',
},
],
hotkey: (state) =>
state.matches('Sketch.Circle tool') ? ['Esc', 'C'] : 'C',
isActive: (state) => state.matches('Sketch.Circle tool'),
},
{
id: 'circle-three-points',

View File

@ -141,12 +141,7 @@ interface Store {
openPanes: SidebarType[]
}
export type SketchTool =
| 'line'
| 'tangentialArc'
| 'rectangle'
| 'circle'
| 'none'
export type SketchTool = 'line' | 'tangentialArc' | 'rectangle' | 'none'
export type ModelingMachineEvent =
| {
@ -214,10 +209,6 @@ export type ModelingMachineEvent =
type: 'Add rectangle origin'
data: [x: number, y: number]
}
| {
type: 'Add circle origin'
data: [x: number, y: number]
}
| {
type: 'done.invoke.animate-to-face' | 'done.invoke.animate-to-sketch'
data: SketchDetails
@ -375,7 +366,7 @@ export const modelingMachine = createMachine(
'Artifact graph emptied': 'hidePlanes',
},
entry: 'show default planes',
entry: ['show default planes', 'reset camera position'],
},
},
@ -600,8 +591,6 @@ export const modelingMachine = createMachine(
Cancel: '#Modeling.Sketch.undo startSketchOn',
},
},
'new state 1': {},
},
initial: 'Init',
@ -807,31 +796,8 @@ export const modelingMachine = createMachine(
target: 'Tangential arc to',
cond: 'next is tangential arc',
},
{
target: 'Circle tool',
cond: 'next is circle',
},
],
},
'Circle tool': {
entry: 'listen for circle origin',
states: {
'Awaiting origin': {
on: {
'Add circle origin': {
target: 'Awaiting perimeter click',
actions: 'set up draft circle',
},
},
},
'Awaiting perimeter click': {},
},
initial: 'Awaiting origin',
},
},
initial: 'Init',
@ -1075,14 +1041,6 @@ export const modelingMachine = createMachine(
if ((state?.event as any).data.tool !== 'rectangle') return false
return canRectangleTool({ sketchDetails })
},
'next is circle': ({ sketchDetails }, _, { state }) => {
if ((state?.event as any).data.tool !== 'circle') return false
// TODO: Both this an rectangle can currently only be used
// if the sketch is empty. They share limited implementations,
// so I'm using the same cond until we have
// multi-profile sketch support in.
return canRectangleTool({ sketchDetails })
},
'next is line': (_, __, { state }) =>
(state?.event as any).data.tool === 'line',
'next is none': (_, __, { state }) =>
@ -1105,6 +1063,8 @@ export const modelingMachine = createMachine(
sketchEnginePathId: '',
sketchPlaneId: '',
}),
'reset camera position': () =>
sceneInfra.camControls.resetCameraPosition(),
'set new sketch metadata': assign((_, { data }) => ({
sketchDetails: data,
})),
@ -1323,18 +1283,13 @@ export const modelingMachine = createMachine(
},
'set up draft rectangle': ({ sketchDetails }, { data }) => {
if (!sketchDetails || !data) return
sceneEntitiesManager.setupDraftRectangle(data, sketchDetails)
},
'listen for circle origin': ({ sketchDetails }) => {
if (!sketchDetails) return
sceneEntitiesManager.setupCircleOriginListener()
},
'set up draft circle': ({ sketchDetails }, { data }) => {
if (!sketchDetails || !data) return
console.log('setting up draft circle', data)
sceneEntitiesManager.setupDraftCircle(data, sketchDetails)
sceneEntitiesManager.setupDraftRectangle(
sketchDetails.sketchPathToNode,
sketchDetails.zAxis,
sketchDetails.yAxis,
sketchDetails.origin,
data
)
},
'set up draft line without teardown': ({ sketchDetails }) => {
if (!sketchDetails) return
@ -1705,10 +1660,7 @@ export function isEditingExistingSketch({
(item) =>
item.type === 'CallExpression' && item.callee.name === 'startProfileAt'
)
const hasCircle = pipeExpression.body.some(
(item) => item.type === 'CallExpression' && item.callee.name === 'circle'
)
return (hasStartProfileAt && pipeExpression.body.length > 2) || hasCircle
return hasStartProfileAt && pipeExpression.body.length > 2
}
export function canRectangleTool({

View File

@ -43,8 +43,8 @@ export default function UserMenu() {
<h2 className="text-2xl font-bold">User Menu</h2>
<p className="my-4">
Click {buttonDescription} in the upper right to open the user menu.
You can change your user-level settings, sign out, or request a
feature.
You can change your user-level settings, sign out, report a bug,
manage your account, request a feature, and more.
</p>
<p className="my-4">
Many settings can be set either a user or per-project level. User