* start of overlay work
* add new icons
* add constraint symbols
* add three dots
* add primary colours
* refactor how we get constraint info for overlays
* refactor how we get constraint info for overlays
* get symbols working for tangential arc too
* extra data on constraint info
* add initial delete
* fix types and circular dep issue after rebase
* fix quirk with horz vert line overlays
* fix setup and tear down of overlays
* remove overlays that are too small
* throttle overlay updates and prove tests selecting html instead of hardcoded px coords
* initial show overaly on segment hover
* remove overlays when tool is equipped
* dounce overlay updates
* tsc
* make higlighting robust to small changes in source ranges
* replace with variable for unconstrained values, and improve styles for popover
* background tweak
* make overlays unconstrain inputs
* fix small regression
* write query for finding related tag references
* make delete segment safe
* typo
* un used imports
* test deleteSegmentFromPipeExpression
* add getConstraintInfo test
* test removeSingleConstraintInfo
* more tests
* tsc
* add tests for overlay buttons
* rename tests
* fmt
* better naming structure
* more reliablity
* more test tweaks
* fix selection test
* add delete segments with overlays tests
* dependant tag tests for segment delet
* typo
* test clean up
* fix some perf issus
* clean up
* clean up
* make things a little more dry
* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)
* trigger ci
* Make constraint hover popovers readable on light mode
* Touch up the new variable dialog
* Little touch-up to three-dot menu style
* fix highlight issue
* fmt
* use optional chain
* Revert "A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)"
This reverts commit be3d61e4a3
.
* A snapshot a day keeps the bugs away! 📷🐛 (OS: ubuntu)
* disable var panel in sketch mode
* fix overlay tests after mergi in main
* test tweak
* try fix ubuntu
* fmt
* more test tweaks
* tweak
* tweaks
---------
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Frank Noirot <frank@kittycad.io>
280 lines
8.8 KiB
TypeScript
280 lines
8.8 KiB
TypeScript
import { test, expect, Page } 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'
|
|
|
|
async function waitForPageLoad(page: Page) {
|
|
// wait for 'Loading stream...' spinner
|
|
await page.getByTestId('loading-stream').waitFor()
|
|
// wait for all spinners to be gone
|
|
await page.getByTestId('loading').waitFor({ state: 'detached' })
|
|
|
|
await page.getByTestId('start-sketch').waitFor()
|
|
}
|
|
|
|
async function removeCurrentCode(page: Page) {
|
|
const hotkey = process.platform === 'darwin' ? 'Meta' : 'Control'
|
|
await page.click('.cm-content')
|
|
await page.keyboard.down(hotkey)
|
|
await page.keyboard.press('a')
|
|
await page.keyboard.up(hotkey)
|
|
await page.keyboard.press('Backspace')
|
|
await expect(page.locator('.cm-content')).toHaveText('')
|
|
}
|
|
|
|
async function sendCustomCmd(page: Page, cmd: EngineCommand) {
|
|
await page.fill('[data-testid="custom-cmd-input"]', JSON.stringify(cmd))
|
|
await page.click('[data-testid="custom-cmd-send-button"]')
|
|
}
|
|
|
|
async function clearCommandLogs(page: Page) {
|
|
await page.click('[data-testid="clear-commands"]')
|
|
}
|
|
|
|
async function expectCmdLog(page: Page, locatorStr: string) {
|
|
await expect(page.locator(locatorStr).last()).toBeVisible()
|
|
}
|
|
|
|
async function waitForDefaultPlanesToBeVisible(page: Page) {
|
|
await page.waitForFunction(
|
|
() =>
|
|
document.querySelectorAll('[data-receive-command-type="object_visible"]')
|
|
.length >= 3
|
|
)
|
|
}
|
|
|
|
async function openKclCodePanel(page: Page) {
|
|
const paneLocator = page.getByRole('tab', { name: 'KCL Code', exact: false })
|
|
const isOpen = (await paneLocator?.getAttribute('aria-selected')) === 'true'
|
|
|
|
if (!isOpen) {
|
|
await paneLocator.click()
|
|
await paneLocator.and(page.locator('[aria-selected="true"]')).waitFor()
|
|
}
|
|
}
|
|
|
|
async function closeKclCodePanel(page: Page) {
|
|
const paneLocator = page.getByRole('tab', { name: 'KCL Code', exact: false })
|
|
const isOpen = (await paneLocator?.getAttribute('aria-selected')) === 'true'
|
|
if (isOpen) {
|
|
await paneLocator.click()
|
|
await paneLocator
|
|
.and(page.locator(':not([aria-selected="true"])'))
|
|
.waitFor()
|
|
}
|
|
}
|
|
|
|
async function openDebugPanel(page: Page) {
|
|
const debugLocator = page.getByRole('tab', { name: 'Debug', exact: false })
|
|
const isOpen = (await debugLocator?.getAttribute('aria-selected')) === 'true'
|
|
|
|
if (!isOpen) {
|
|
await debugLocator.click()
|
|
await debugLocator.and(page.locator('[aria-selected="true"]')).waitFor()
|
|
}
|
|
}
|
|
|
|
async function closeDebugPanel(page: Page) {
|
|
const debugLocator = page.getByRole('tab', { name: 'Debug', exact: false })
|
|
const isOpen = (await debugLocator?.getAttribute('aria-selected')) === 'true'
|
|
if (isOpen) {
|
|
await debugLocator.click()
|
|
await debugLocator
|
|
.and(page.locator(':not([aria-selected="true"])'))
|
|
.waitFor()
|
|
}
|
|
}
|
|
|
|
async function waitForCmdReceive(page: Page, commandType: string) {
|
|
return page
|
|
.locator(`[data-receive-command-type="${commandType}"]`)
|
|
.first()
|
|
.waitFor()
|
|
}
|
|
|
|
export async function getUtils(page: Page) {
|
|
const cdpSession =
|
|
process.platform === 'darwin'
|
|
? null
|
|
: await page.context().newCDPSession(page)
|
|
|
|
return {
|
|
waitForAuthSkipAppStart: () => waitForPageLoad(page),
|
|
removeCurrentCode: () => removeCurrentCode(page),
|
|
sendCustomCmd: (cmd: EngineCommand) => sendCustomCmd(page, cmd),
|
|
updateCamPosition: async (xyz: [number, number, number]) => {
|
|
const fillInput = async (axis: 'x' | 'y' | 'z', value: number) => {
|
|
await page.fill(`[data-testid="cam-${axis}-position"]`, String(value))
|
|
await page.waitForTimeout(100)
|
|
}
|
|
|
|
await fillInput('x', xyz[0])
|
|
await fillInput('y', xyz[1])
|
|
await fillInput('z', xyz[2])
|
|
},
|
|
clearCommandLogs: () => clearCommandLogs(page),
|
|
expectCmdLog: (locatorStr: string) => expectCmdLog(page, locatorStr),
|
|
openKclCodePanel: () => openKclCodePanel(page),
|
|
closeKclCodePanel: () => closeKclCodePanel(page),
|
|
openDebugPanel: () => openDebugPanel(page),
|
|
closeDebugPanel: () => closeDebugPanel(page),
|
|
openAndClearDebugPanel: async () => {
|
|
await openDebugPanel(page)
|
|
return clearCommandLogs(page)
|
|
},
|
|
clearAndCloseDebugPanel: async () => {
|
|
await clearCommandLogs(page)
|
|
return closeDebugPanel(page)
|
|
},
|
|
waitForCmdReceive: (commandType: string) =>
|
|
waitForCmdReceive(page, commandType),
|
|
getBoundingBox: async (locator: string) =>
|
|
page
|
|
.locator(locator)
|
|
.boundingBox()
|
|
.then((box) => ({ x: box?.x || 0, y: box?.y || 0 })),
|
|
doAndWaitForCmd: async (
|
|
fn: () => Promise<void>,
|
|
commandType: string,
|
|
endWithDebugPanelOpen = true
|
|
) => {
|
|
await openDebugPanel(page)
|
|
await clearCommandLogs(page)
|
|
await closeDebugPanel(page)
|
|
await fn()
|
|
await openDebugPanel(page)
|
|
await waitForCmdReceive(page, commandType)
|
|
if (!endWithDebugPanelOpen) {
|
|
await closeDebugPanel(page)
|
|
}
|
|
},
|
|
doAndWaitForImageDiff: (fn: () => Promise<any>, diffCount = 200) =>
|
|
new Promise(async (resolve) => {
|
|
await page.screenshot({
|
|
path: './e2e/playwright/temp1.png',
|
|
fullPage: true,
|
|
})
|
|
await fn()
|
|
const isImageDiff = async () => {
|
|
await page.screenshot({
|
|
path: './e2e/playwright/temp2.png',
|
|
fullPage: true,
|
|
})
|
|
const screenshot1 = PNG.sync.read(
|
|
await fsp.readFile('./e2e/playwright/temp1.png')
|
|
)
|
|
const screenshot2 = PNG.sync.read(
|
|
await fsp.readFile('./e2e/playwright/temp2.png')
|
|
)
|
|
const actualDiffCount = pixelMatch(
|
|
screenshot1.data,
|
|
screenshot2.data,
|
|
null,
|
|
screenshot1.width,
|
|
screenshot2.height
|
|
)
|
|
return actualDiffCount > diffCount
|
|
}
|
|
|
|
// run isImageDiff every 50ms until it returns true or 5 seconds have passed (100 times)
|
|
let count = 0
|
|
const interval = setInterval(async () => {
|
|
count++
|
|
if (await isImageDiff()) {
|
|
clearInterval(interval)
|
|
resolve(true)
|
|
} else if (count > 100) {
|
|
clearInterval(interval)
|
|
resolve(false)
|
|
}
|
|
}, 50)
|
|
}),
|
|
emulateNetworkConditions: async (
|
|
networkOptions: Protocol.Network.emulateNetworkConditionsParameters
|
|
) => {
|
|
// Skip on non-Chromium browsers, since we need to use the CDP.
|
|
test.skip(
|
|
cdpSession === null,
|
|
'Network emulation is only supported in Chromium'
|
|
)
|
|
|
|
cdpSession?.send('Network.emulateNetworkConditions', networkOptions)
|
|
},
|
|
}
|
|
}
|
|
|
|
type TemplateOptions = Array<number | Array<number>>
|
|
|
|
type makeTemplateReturn = {
|
|
regExp: RegExp
|
|
genNext: (
|
|
templateParts: TemplateStringsArray,
|
|
...options: TemplateOptions
|
|
) => makeTemplateReturn
|
|
}
|
|
|
|
const escapeRegExp = (string: string) => {
|
|
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&') // $& means the whole matched string
|
|
}
|
|
|
|
const _makeTemplate = (
|
|
templateParts: TemplateStringsArray,
|
|
...options: TemplateOptions
|
|
) => {
|
|
const length = Math.max(...options.map((a) => (Array.isArray(a) ? a[0] : 0)))
|
|
let reExpTemplate = ''
|
|
for (let i = 0; i < length; i++) {
|
|
const currentStr = templateParts.map((str, index) => {
|
|
const currentOptions = options[index]
|
|
return (
|
|
escapeRegExp(str) +
|
|
String(
|
|
Array.isArray(currentOptions)
|
|
? currentOptions[i]
|
|
: typeof currentOptions === 'number'
|
|
? currentOptions
|
|
: ''
|
|
)
|
|
)
|
|
})
|
|
reExpTemplate += '|' + currentStr.join('')
|
|
}
|
|
return new RegExp(reExpTemplate)
|
|
}
|
|
|
|
/**
|
|
* Tool for making templates to match code snippets in the editor with some fudge factor,
|
|
* as there's some level of non-determinism.
|
|
*
|
|
* Usage is as such:
|
|
* ```typescript
|
|
* const result = makeTemplate`const myVar = aFunc(${[1, 2, 3]})`
|
|
* await expect(page.locator('.cm-content')).toHaveText(result.regExp)
|
|
* ```
|
|
* Where the value `1`, `2` or `3` are all valid and should make the test pass.
|
|
*
|
|
* The function also has a `genNext` function that allows you to chain multiple templates
|
|
* together without having to repeat previous parts of the template.
|
|
* ```typescript
|
|
* const result2 = result.genNext`const myVar2 = aFunc(${[4, 5, 6]})`
|
|
* ```
|
|
*/
|
|
export const makeTemplate: (
|
|
templateParts: TemplateStringsArray,
|
|
...values: TemplateOptions
|
|
) => makeTemplateReturn = (templateParts, ...options) => {
|
|
return {
|
|
regExp: _makeTemplate(templateParts, ...options),
|
|
genNext: (
|
|
nextTemplateParts: TemplateStringsArray,
|
|
...nextOptions: TemplateOptions
|
|
) =>
|
|
makeTemplate(
|
|
[...templateParts, ...nextTemplateParts] as any as TemplateStringsArray,
|
|
[...options, ...nextOptions] as any
|
|
),
|
|
}
|
|
}
|