2024-08-07 19:27:32 +10:00
import {
expect ,
Page ,
Download ,
BrowserContext ,
2024-08-22 13:38:53 -04:00
TestInfo ,
2024-08-16 07:15:42 -04:00
_electron as electron ,
2024-08-20 05:34:26 +10:00
Locator ,
2024-08-22 13:38:53 -04:00
test ,
2024-08-07 19:27:32 +10:00
} from '@playwright/test'
2024-08-03 18:08:51 +10:00
import { EngineCommand } from 'lang/std/artifactGraph'
2024-06-04 14:36:34 -04:00
import os from 'os'
2023-12-01 20:49:12 +11:00
import fsp from 'fs/promises'
2024-08-16 07:15:42 -04:00
import fsSync from 'fs'
import { join } from 'path'
2023-12-01 20:49:12 +11:00
import pixelMatch from 'pixelmatch'
import { PNG } from 'pngjs'
2024-05-23 02:20:40 -07:00
import { Protocol } from 'playwright-core/types/protocol'
2024-05-29 18:04:27 -04:00
import type { Models } from '@kittycad/lib'
2024-08-14 14:26:44 -04:00
import { APP_NAME , COOKIE_NAME } from 'lib/constants'
2024-08-07 19:27:32 +10:00
import { secrets } from './secrets'
2024-08-16 07:15:42 -04:00
import {
TEST_SETTINGS_KEY ,
TEST_SETTINGS ,
IS_PLAYWRIGHT_KEY ,
} from './storageStates'
2024-08-07 19:27:32 +10:00
import * as TOML from '@iarna/toml'
2024-08-16 07:15:42 -04:00
import { SaveSettingsPayload } from 'lib/settings/settingsTypes'
import { SETTINGS_FILE_NAME } from 'lib/constants'
2023-11-24 08:59:24 +11:00
2024-06-22 04:49:31 -04:00
type TestColor = [ number , number , number ]
export const TEST_COLORS = {
WHITE : [ 249 , 249 , 249 ] as TestColor ,
YELLOW : [ 255 , 255 , 0 ] as TestColor ,
BLUE : [ 0 , 0 , 255 ] as TestColor ,
} as const
2024-08-07 19:27:32 +10:00
export const PERSIST_MODELING_CONTEXT = 'persistModelingContext'
export const deg = ( Math . PI * 2 ) / 360
export const commonPoints = {
startAt : '[7.19, -9.7]' ,
num1 : 7.25 ,
num2 : 14.44 ,
}
2024-08-22 13:38:53 -04:00
export const editorSelector = '[role="textbox"][data-language="kcl"]'
type PaneId = 'variables' | 'code' | 'files' | 'logs'
2024-08-05 21:30:16 +10:00
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 (
2024-08-16 07:15:42 -04:00
page . getByRole ( 'button' , { name : 'sketch Start Sketch' } ) ,
2024-08-05 21:30:16 +10:00
errorMessage
) . toBeEnabled ( {
timeout : 20_000 ,
} )
} ) . toPass ( { timeout : 70_000 , intervals : [ 1 _000 ] } )
}
2023-11-24 08:59:24 +11:00
async function waitForPageLoad ( page : Page ) {
// wait for all spinners to be gone
2024-07-18 16:16:17 -04:00
await expect ( page . getByTestId ( 'loading' ) ) . not . toBeAttached ( {
timeout : 20_000 ,
} )
2023-11-24 08:59:24 +11:00
2024-07-24 23:33:31 -04:00
await expect ( page . getByRole ( 'button' , { name : 'Start Sketch' } ) ) . toBeEnabled ( {
2024-07-18 16:16:17 -04:00
timeout : 20_000 ,
} )
2023-11-24 08:59:24 +11:00
}
async function removeCurrentCode ( page : Page ) {
const hotkey = process . platform === 'darwin' ? 'Meta' : 'Control'
2024-06-07 10:48:42 +10:00
await page . locator ( '.cm-content' ) . click ( )
2023-11-24 08:59:24 +11:00
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 ) {
2024-06-07 10:48:42 +10:00
await page . getByTestId ( 'custom-cmd-input' ) . fill ( JSON . stringify ( cmd ) )
await page . getByTestId ( 'custom-cmd-send-button' ) . click ( )
2023-11-24 08:59:24 +11:00
}
async function clearCommandLogs ( page : Page ) {
2024-06-07 10:48:42 +10:00
await page . getByTestId ( 'clear-commands' ) . click ( )
2023-11-24 08:59:24 +11:00
}
2024-06-29 10:36:04 -07:00
async function expectCmdLog ( page : Page , locatorStr : string , timeout = 5000 ) {
await expect ( page . locator ( locatorStr ) . last ( ) ) . toBeVisible ( { timeout } )
2023-11-24 08:59:24 +11:00
}
2024-08-19 15:36:18 -04:00
// Ignoring the lint since I assume someone will want to use this for a test.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
2023-11-24 08:59:24 +11:00
async function waitForDefaultPlanesToBeVisible ( page : Page ) {
await page . waitForFunction (
( ) = >
document . querySelectorAll ( '[data-receive-command-type="object_visible"]' )
. length >= 3
)
}
2024-08-20 22:11:21 -04:00
async function openPane ( page : Page , testId : string ) {
const locator = page . getByTestId ( testId )
await expect ( locator ) . toBeVisible ( )
const isOpen = ( await locator ? . getAttribute ( 'aria-pressed' ) ) === 'true'
2024-04-15 12:04:17 -04:00
if ( ! isOpen ) {
2024-08-20 22:11:21 -04:00
await locator . click ( )
await expect ( locator ) . toHaveAttribute ( 'aria-pressed' , 'true' )
2024-04-15 12:04:17 -04:00
}
}
2024-08-20 22:11:21 -04:00
async function openKclCodePanel ( page : Page ) {
await openPane ( page , 'code-pane-button' )
}
2024-04-15 12:04:17 -04:00
async function closeKclCodePanel ( page : Page ) {
2024-07-24 23:33:31 -04:00
const paneLocator = page . getByTestId ( 'code-pane-button' )
const ariaSelected = await paneLocator ? . getAttribute ( 'aria-pressed' )
const isOpen = ariaSelected === 'true'
2024-04-15 12:04:17 -04:00
if ( isOpen ) {
await paneLocator . click ( )
2024-07-24 22:02:16 -04:00
await expect ( paneLocator ) . not . toHaveAttribute ( 'aria-pressed' , 'true' )
2024-04-15 12:04:17 -04:00
}
}
2023-11-24 08:59:24 +11:00
async function openDebugPanel ( page : Page ) {
2024-08-20 22:11:21 -04:00
await openPane ( page , 'debug-pane-button' )
2023-11-24 08:59:24 +11:00
}
async function closeDebugPanel ( page : Page ) {
2024-07-24 23:33:31 -04:00
const debugLocator = page . getByTestId ( 'debug-pane-button' )
await expect ( debugLocator ) . toBeVisible ( )
2024-07-24 22:02:16 -04:00
const isOpen = ( await debugLocator ? . getAttribute ( 'aria-pressed' ) ) === 'true'
2023-11-24 08:59:24 +11:00
if ( isOpen ) {
2024-04-15 12:04:17 -04:00
await debugLocator . click ( )
2024-07-24 22:02:16 -04:00
await expect ( debugLocator ) . not . toHaveAttribute ( 'aria-pressed' , 'true' )
2023-11-24 08:59:24 +11:00
}
}
2024-08-19 14:21:34 -07:00
async function openFilePanel ( page : Page ) {
2024-08-20 22:11:21 -04:00
await openPane ( page , 'files-pane-button' )
2024-08-19 14:21:34 -07:00
}
async function closeFilePanel ( page : Page ) {
const fileLocator = page . getByTestId ( 'files-pane-button' )
await expect ( fileLocator ) . toBeVisible ( )
const isOpen = ( await fileLocator ? . getAttribute ( 'aria-pressed' ) ) === 'true'
if ( isOpen ) {
await fileLocator . click ( )
await expect ( fileLocator ) . not . toHaveAttribute ( 'aria-pressed' , 'true' )
}
}
2024-08-20 22:11:21 -04:00
async function openVariablesPane ( page : Page ) {
await openPane ( page , 'variables-pane-button' )
}
async function openLogsPane ( page : Page ) {
await openPane ( page , 'logs-pane-button' )
}
2023-11-24 08:59:24 +11:00
async function waitForCmdReceive ( page : Page , commandType : string ) {
return page
. locator ( ` [data-receive-command-type=" ${ commandType } "] ` )
. first ( )
. waitFor ( )
}
2024-06-04 08:32:24 -04:00
export const wiggleMove = async (
page : any ,
x : number ,
y : number ,
steps : number ,
dist : number ,
ang : number ,
amplitude : number ,
2024-06-24 11:45:40 -04:00
freq : number ,
locator? : string
2024-06-04 08:32:24 -04:00
) = > {
const tau = Math . PI * 2
const deg = tau / 360
const step = dist / steps
for ( let i = 0 , j = 0 ; i < dist ; i += step , j += 1 ) {
2024-06-24 11:45:40 -04:00
if ( locator ) {
const isElVis = await page . locator ( locator ) . isVisible ( )
if ( isElVis ) return
}
2024-08-19 15:36:18 -04:00
// x1 is 0.
const y1 = Math . sin ( ( tau / steps ) * j * freq ) * amplitude
2024-06-04 08:32:24 -04:00
const [ x2 , y2 ] = [
Math . cos ( - ang * deg ) * i - Math . sin ( - ang * deg ) * y1 ,
Math . sin ( - ang * deg ) * i + Math . cos ( - ang * deg ) * y1 ,
]
const [ xr , yr ] = [ x2 , y2 ]
2024-06-24 11:45:40 -04:00
await page . mouse . move ( x + xr , y + yr , { steps : 5 } )
}
}
export const circleMove = async (
2024-08-22 15:00:13 -05:00
page : Page ,
2024-06-24 11:45:40 -04:00
x : number ,
y : number ,
steps : number ,
diameter : number ,
locator? : string
) = > {
const tau = Math . PI * 2
const step = tau / steps
for ( let i = 0 ; i < tau ; i += step ) {
if ( locator ) {
const isElVis = await page . locator ( locator ) . isVisible ( )
if ( isElVis ) return
}
const [ x1 , y1 ] = [ Math . cos ( i ) * diameter , Math . sin ( i ) * diameter ]
const [ xr , yr ] = [ x1 , y1 ]
await page . mouse . move ( x + xr , y + yr , { steps : 5 } )
2024-06-04 08:32:24 -04:00
}
}
export const getMovementUtils = ( opts : any ) = > {
// The way we truncate is kinda odd apparently, so we need this function
// "[k]itty[c]ad round"
const kcRound = ( n : number ) = > Math . trunc ( n * 100 ) / 100
// To translate between screen and engine ("[U]nit") coordinates
// NOTE: these pretty much can't be perfect because of screen scaling.
// Handle on a case-by-case.
const toU = ( x : number , y : number ) = > [
2024-06-18 16:08:41 +10:00
kcRound ( x * 0.0678 ) ,
kcRound ( - y * 0.0678 ) , // Y is inverted in our coordinate system
2024-06-04 08:32:24 -04:00
]
// Turn the array into a string with specific formatting
const fromUToString = ( xy : number [ ] ) = > ` [ ${ xy [ 0 ] } , ${ xy [ 1 ] } ] `
// Combine because used often
const toSU = ( xy : number [ ] ) = > fromUToString ( toU ( xy [ 0 ] , xy [ 1 ] ) )
// Make it easier to click around from center ("click [from] zero zero")
const click00 = ( x : number , y : number ) = >
2024-06-24 11:45:40 -04:00
opts . page . mouse . click ( opts . center . x + x , opts . center . y + y , { delay : 100 } )
2024-06-04 08:32:24 -04:00
// Relative clicker, must keep state
let last = { x : 0 , y : 0 }
2024-06-24 11:45:40 -04:00
const click00r = async ( x? : number , y? : number ) = > {
2024-06-04 08:32:24 -04:00
// reset relative coordinates when anything is undefined
if ( x === undefined || y === undefined ) {
last . x = 0
last . y = 0
return
}
2024-06-24 11:45:40 -04:00
await circleMove (
opts . page ,
opts . center . x + last . x + x ,
opts . center . y + last . y + y ,
10 ,
10
)
await click00 ( last . x + x , last . y + y )
2024-06-04 08:32:24 -04:00
last . x += x
last . y += y
// Returns the new absolute coordinate if you need it.
2024-06-24 11:45:40 -04:00
return [ last . x , last . y ]
2024-06-04 08:32:24 -04:00
}
2024-06-05 14:43:12 +02:00
return { toSU , click00r }
2024-06-04 08:32:24 -04:00
}
2024-06-29 18:10:07 -07:00
async function waitForAuthAndLsp ( page : Page ) {
2024-08-08 09:19:07 +10:00
const waitForLspPromise = page . waitForEvent ( 'console' , {
predicate : async ( message ) = > {
// it would be better to wait for a message that the kcl lsp has started by looking for the message message.text().includes('[lsp] [window/logMessage]')
// but that doesn't seem to make it to the console for macos/safari :(
if ( message . text ( ) . includes ( 'start kcl lsp' ) ) {
await new Promise ( ( resolve ) = > setTimeout ( resolve , 200 ) )
return true
}
return false
} ,
timeout : 45_000 ,
2024-06-29 18:10:07 -07:00
} )
2024-08-05 21:30:16 +10:00
if ( process . env . CI ) {
await waitForPageLoadWithRetry ( page )
} else {
await page . goto ( '/' )
await waitForPageLoad ( page )
}
2024-06-29 18:10:07 -07:00
return waitForLspPromise
}
2024-08-12 15:16:13 +10:00
export function normaliseKclNumbers ( code : string , ignoreZero = true ) : string {
const numberRegexp = /(?<!\w)-?\b\d+(\.\d+)?\b(?!\w)/g
const replaceNumber = ( number : string ) = > {
if ( ignoreZero && ( number === '0' || number === '-0' ) ) return number
const sign = number . startsWith ( '-' ) ? '-' : ''
return ` ${ sign } 12.34 `
}
const replaceNumbers = ( text : string ) = >
text . replace ( numberRegexp , replaceNumber )
return replaceNumbers ( code )
}
2024-08-22 13:38:53 -04:00
export async function getUtils ( page : Page , test_? : typeof test ) {
if ( ! test ) {
console . warn (
'Some methods in getUtils requires test object as second argument'
)
}
2024-05-24 16:11:49 -07:00
// Chrome devtools protocol session only works in Chromium
const browserType = page . context ( ) . browser ( ) ? . browserType ( ) . name ( )
2024-05-23 02:20:40 -07:00
const cdpSession =
2024-05-24 16:11:49 -07:00
browserType !== 'chromium' ? null : await page . context ( ) . newCDPSession ( page )
2024-05-23 02:20:40 -07:00
2024-08-22 13:38:53 -04:00
const util = {
2024-06-29 18:10:07 -07:00
waitForAuthSkipAppStart : ( ) = > waitForAuthAndLsp ( page ) ,
2024-07-05 05:42:54 +10:00
waitForPageLoad : ( ) = > waitForPageLoad ( page ) ,
2024-08-05 21:30:16 +10:00
waitForPageLoadWithRetry : ( ) = > waitForPageLoadWithRetry ( page ) ,
2023-11-24 08:59:24 +11:00
removeCurrentCode : ( ) = > removeCurrentCode ( page ) ,
sendCustomCmd : ( cmd : EngineCommand ) = > sendCustomCmd ( page , cmd ) ,
2024-02-11 12:59:00 +11:00
updateCamPosition : async ( xyz : [ number , number , number ] ) = > {
2024-04-15 12:04:17 -04:00
const fillInput = async ( axis : 'x' | 'y' | 'z' , value : number ) = > {
await page . fill ( ` [data-testid="cam- ${ axis } -position"] ` , String ( value ) )
await page . waitForTimeout ( 100 )
2024-02-11 12:59:00 +11:00
}
2024-04-15 12:04:17 -04:00
await fillInput ( 'x' , xyz [ 0 ] )
await fillInput ( 'y' , xyz [ 1 ] )
await fillInput ( 'z' , xyz [ 2 ] )
2024-02-11 12:59:00 +11:00
} ,
2023-11-24 08:59:24 +11:00
clearCommandLogs : ( ) = > clearCommandLogs ( page ) ,
2024-06-29 10:36:04 -07:00
expectCmdLog : ( locatorStr : string , timeout = 5000 ) = >
expectCmdLog ( page , locatorStr , timeout ) ,
2024-04-15 12:04:17 -04:00
openKclCodePanel : ( ) = > openKclCodePanel ( page ) ,
closeKclCodePanel : ( ) = > closeKclCodePanel ( page ) ,
2023-11-24 08:59:24 +11:00
openDebugPanel : ( ) = > openDebugPanel ( page ) ,
closeDebugPanel : ( ) = > closeDebugPanel ( page ) ,
2024-08-19 14:21:34 -07:00
openFilePanel : ( ) = > openFilePanel ( page ) ,
closeFilePanel : ( ) = > closeFilePanel ( page ) ,
2024-08-20 22:11:21 -04:00
openVariablesPane : ( ) = > openVariablesPane ( page ) ,
openLogsPane : ( ) = > openLogsPane ( page ) ,
2023-11-24 08:59:24 +11:00
openAndClearDebugPanel : async ( ) = > {
await openDebugPanel ( page )
return clearCommandLogs ( page )
} ,
clearAndCloseDebugPanel : async ( ) = > {
await clearCommandLogs ( page )
return closeDebugPanel ( page )
} ,
waitForCmdReceive : ( commandType : string ) = >
waitForCmdReceive ( page , commandType ) ,
2024-05-31 11:36:08 +10:00
getSegmentBodyCoords : async ( locator : string , px = 30 ) = > {
const overlay = page . locator ( locator )
const bbox = await overlay
2024-08-16 07:15:42 -04:00
. boundingBox ( { timeout : 5_000 } )
2024-05-31 11:36:08 +10:00
. then ( ( box ) = > ( { . . . box , x : box?.x || 0 , y : box?.y || 0 } ) )
const angle = Number ( await overlay . getAttribute ( 'data-overlay-angle' ) )
const angleXOffset = Math . cos ( ( ( angle - 180 ) * Math . PI ) / 180 ) * px
const angleYOffset = Math . sin ( ( ( angle - 180 ) * Math . PI ) / 180 ) * px
return {
2024-06-05 10:36:12 +10:00
x : Math.round ( bbox . x + angleXOffset ) ,
y : Math.round ( bbox . y - angleYOffset ) ,
2024-05-31 11:36:08 +10:00
}
} ,
2024-06-04 08:32:24 -04:00
getAngle : async ( locator : string ) = > {
const overlay = page . locator ( locator )
return Number ( await overlay . getAttribute ( 'data-overlay-angle' ) )
} ,
2024-05-24 20:54:42 +10:00
getBoundingBox : async ( locator : string ) = >
page
. locator ( locator )
2024-08-16 07:15:42 -04:00
. boundingBox ( { timeout : 5_000 } )
2024-06-04 08:32:24 -04:00
. then ( ( box ) = > ( { . . . box , x : box?.x || 0 , y : box?.y || 0 } ) ) ,
2024-06-05 14:43:12 +02:00
codeLocator : page.locator ( '.cm-content' ) ,
2024-08-12 15:16:13 +10:00
normalisedEditorCode : async ( ) = > {
const code = await page . locator ( '.cm-content' ) . innerText ( )
return normaliseKclNumbers ( code )
} ,
normalisedCode : ( code : string ) = > normaliseKclNumbers ( code ) ,
2024-06-18 16:08:41 +10:00
canvasLocator : page.getByTestId ( 'client-side-scene' ) ,
2023-11-24 08:59:24 +11:00
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 )
}
} ,
2024-06-05 10:36:12 +10:00
/ * *
* Given an expected RGB value , diff if the channel with the largest difference
* /
getGreatestPixDiff : async (
coords : { x : number ; y : number } ,
expected : [ number , number , number ]
) : Promise < number > = > {
const buffer = await page . screenshot ( {
fullPage : true ,
} )
const screenshot = await PNG . sync . read ( buffer )
2024-07-07 13:10:52 -04:00
const pixMultiplier : number = await page . evaluate (
'window.devicePixelRatio'
)
2024-06-05 10:36:12 +10:00
const index =
( screenshot . width * coords . y * pixMultiplier +
coords . x * pixMultiplier ) *
4 // rbga is 4 channels
2024-06-28 14:40:59 +10:00
const maxDiff = Math . max (
2024-06-05 10:36:12 +10:00
Math . abs ( screenshot . data [ index ] - expected [ 0 ] ) ,
Math . abs ( screenshot . data [ index + 1 ] - expected [ 1 ] ) ,
Math . abs ( screenshot . data [ index + 2 ] - expected [ 2 ] )
)
2024-06-28 14:40:59 +10:00
if ( maxDiff > 4 ) {
console . log (
` Expected: ${ expected } Actual: [ ${ screenshot . data [ index ] } , ${
screenshot . data [ index + 1 ]
} , $ { screenshot . data [ index + 2 ] } ] `
)
}
return maxDiff
2024-06-05 10:36:12 +10:00
} ,
2023-12-01 20:49:12 +11:00
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 )
} ) ,
2024-05-23 02:20:40 -07:00
emulateNetworkConditions : async (
networkOptions : Protocol.Network.emulateNetworkConditionsParameters
) = > {
2024-07-07 13:10:52 -04:00
if ( cdpSession === null ) {
// Use a fail safe if we can't simulate disconnect (on Safari)
return page . evaluate ( 'window.tearDown()' )
}
2024-05-23 02:20:40 -07:00
2024-08-19 15:36:18 -04:00
return cdpSession ? . send (
'Network.emulateNetworkConditions' ,
networkOptions
)
2024-05-23 02:20:40 -07:00
} ,
2024-08-22 13:38:53 -04:00
toNormalizedCode : ( text : string ) = > {
return text . replace ( /\s+/g , '' )
} ,
createAndSelectProject : async ( hasText : string ) = > {
return test_ ? . step (
` Create and select project with text " ${ hasText } " ` ,
async ( ) = > {
await page . getByTestId ( 'home-new-file' ) . click ( )
const projectLinksPost = page . getByTestId ( 'project-link' )
await projectLinksPost . filter ( { hasText } ) . click ( )
}
)
} ,
editorTextMatches : async ( code : string ) = > {
const editor = page . locator ( editorSelector )
const editorText = await editor . textContent ( )
return expect ( util . toNormalizedCode ( editorText || '' ) ) . toBe (
util . toNormalizedCode ( code )
)
} ,
pasteCodeInEditor : async ( code : string ) = > {
return test ? . step ( 'Paste in KCL code' , async ( ) = > {
const editor = page . locator ( editorSelector )
await editor . fill ( code )
await util . editorTextMatches ( code )
} )
} ,
clickPane : async ( paneId : PaneId ) = > {
return test ? . step ( ` Open ${ paneId } pane ` , async ( ) = > {
await page . getByTestId ( paneId + '-pane-button' ) . click ( )
await expect ( page . locator ( '#' + paneId + '-pane' ) ) . toBeVisible ( )
} )
} ,
createNewFileAndSelect : async ( name : string ) = > {
return test ? . step ( ` Create a file named ${ name } , select it ` , async ( ) = > {
await page . getByTestId ( 'create-file-button' ) . click ( )
await page . getByTestId ( 'file-rename-field' ) . fill ( name )
await page . keyboard . press ( 'Enter' )
await page
. getByTestId ( 'file-pane-scroll-container' )
. filter ( { hasText : name } )
. click ( )
} )
} ,
panesOpen : async ( paneIds : PaneId [ ] ) = > {
return test ? . step ( ` Setting ${ paneIds } panes to be open ` , async ( ) = > {
await page . addInitScript (
( { PERSIST_MODELING_CONTEXT , paneIds } ) = > {
localStorage . setItem (
PERSIST_MODELING_CONTEXT ,
JSON . stringify ( { openPanes : paneIds } )
)
} ,
{ PERSIST_MODELING_CONTEXT , paneIds }
)
await page . reload ( )
} )
} ,
2023-11-24 08:59:24 +11:00
}
2024-08-22 13:38:53 -04:00
return util
2023-11-24 08:59:24 +11:00
}
2024-05-02 15:26:23 +10:00
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
) ,
}
}
2024-05-29 18:04:27 -04:00
export interface Paths {
modelPath : string
imagePath : string
outputType : string
}
export const doExport = async (
output : Models [ 'OutputFormat_type' ] ,
2024-08-19 16:29:44 +10:00
page : Page ,
2024-08-21 15:02:39 +10:00
exportFrom : 'dropdown' | 'sidebarButton' | 'commandBar' = 'dropdown'
2024-05-29 18:04:27 -04:00
) : Promise < Paths > = > {
2024-08-21 15:02:39 +10:00
if ( exportFrom === 'dropdown' ) {
2024-08-19 16:29:44 +10:00
await page . getByRole ( 'button' , { name : APP_NAME } ) . click ( )
const exportMenuButton = page . getByRole ( 'button' , {
name : 'Export current part' ,
} )
await expect ( exportMenuButton ) . toBeVisible ( )
await exportMenuButton . click ( )
2024-08-21 15:02:39 +10:00
} else if ( exportFrom === 'sidebarButton' ) {
await expect ( page . getByTestId ( 'export-pane-button' ) ) . toBeVisible ( )
2024-08-19 16:29:44 +10:00
await page . getByTestId ( 'export-pane-button' ) . click ( )
2024-08-21 15:02:39 +10:00
} else if ( exportFrom === 'commandBar' ) {
const commandBarButton = page . getByRole ( 'button' , { name : 'Commands' } )
await expect ( commandBarButton ) . toBeVisible ( )
// Click the command bar button
await commandBarButton . click ( )
// Wait for the command bar to appear
const cmdSearchBar = page . getByPlaceholder ( 'Search commands' )
await expect ( cmdSearchBar ) . toBeVisible ( )
const textToCadCommand = page . getByRole ( 'option' , {
name : 'floppy disk arrow Export' ,
} )
await expect ( textToCadCommand . first ( ) ) . toBeVisible ( )
// Click the Text-to-CAD command
await textToCadCommand . first ( ) . click ( )
2024-08-19 16:29:44 +10:00
}
2024-05-29 18:04:27 -04:00
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
2024-08-21 15:02:39 +10:00
if ( exportFrom === 'dropdown' )
2024-08-19 16:29:44 +10:00
page . on ( 'download' , async ( download ) = > {
if ( downloadCnt === 0 ) {
downloadResolve1 ( download )
}
downloadCnt ++
} )
2024-05-29 18:04:27 -04:00
await page . getByRole ( 'button' , { name : 'Submit command' } ) . click ( )
2024-08-21 15:02:39 +10:00
if ( exportFrom === 'sidebarButton' || exportFrom === 'commandBar' ) {
2024-08-19 16:29:44 +10:00
return {
modelPath : '' ,
imagePath : '' ,
outputType : output.type ,
}
}
2024-05-29 18:04:27 -04:00
// 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 ,
}
}
2024-06-04 14:36:34 -04:00
/ * *
* Gets the appropriate modifier key for the platform .
* /
export const metaModifier = os . platform ( ) === 'darwin' ? 'Meta' : 'Control'
2024-08-07 19:27:32 +10:00
export async function tearDown ( page : Page , testInfo : TestInfo ) {
if ( testInfo . status === 'skipped' ) return
if ( testInfo . status === 'failed' ) return
const u = await getUtils ( page )
// Kill the network so shutdown happens properly
await u . emulateNetworkConditions ( {
offline : true ,
// values of 0 remove any active throttling. crbug.com/456324#c9
latency : 0 ,
downloadThroughput : - 1 ,
uploadThroughput : - 1 ,
} )
// It seems it's best to give the browser about 3s to close things
// It's not super reliable but we have no real other choice for now
await page . waitForTimeout ( 3000 )
}
2024-08-16 07:15:42 -04:00
// settingsOverrides may need to be augmented to take more generic items,
// but we'll be strict for now
2024-08-07 19:27:32 +10:00
export async function setup ( context : BrowserContext , page : Page ) {
await context . addInitScript (
2024-08-16 07:15:42 -04:00
async ( { token , settingsKey , settings , IS_PLAYWRIGHT_KEY } ) = > {
2024-08-20 22:11:21 -04:00
localStorage . clear ( )
2024-08-07 19:27:32 +10:00
localStorage . setItem ( 'TOKEN_PERSIST_KEY' , token )
localStorage . setItem ( 'persistCode' , ` ` )
localStorage . setItem ( settingsKey , settings )
2024-08-16 07:15:42 -04:00
localStorage . setItem ( IS_PLAYWRIGHT_KEY , 'true' )
console . log ( 'TEST_SETTINGS.projects' , settings )
2024-08-07 19:27:32 +10:00
} ,
{
token : secrets.token ,
settingsKey : TEST_SETTINGS_KEY ,
2024-08-16 07:15:42 -04:00
settings : TOML.stringify ( {
settings : {
. . . TEST_SETTINGS ,
app : {
. . . TEST_SETTINGS . projects ,
projectDirectory : TEST_SETTINGS.app.projectDirectory ,
onboardingStatus : 'dismissed' ,
theme : 'dark' ,
} ,
} as Partial < SaveSettingsPayload > ,
} ) ,
IS_PLAYWRIGHT_KEY ,
2024-08-07 19:27:32 +10:00
}
)
2024-08-14 14:26:44 -04:00
await context . addCookies ( [
{
name : COOKIE_NAME ,
value : secrets.token ,
path : '/' ,
domain : 'localhost' ,
secure : true ,
} ,
] )
2024-08-07 19:27:32 +10:00
// kill animations, speeds up tests and reduced flakiness
await page . emulateMedia ( { reducedMotion : 'reduce' } )
2024-08-20 22:11:21 -04:00
2024-08-22 13:38:53 -04:00
// Trigger a navigation, since loading file:// doesn't.
2024-08-20 22:11:21 -04:00
await page . reload ( )
2024-08-07 19:27:32 +10:00
}
2024-08-16 07:15:42 -04:00
export async function setupElectron ( {
testInfo ,
folderSetupFn ,
2024-08-16 15:24:36 -04:00
cleanProjectDir = true ,
2024-08-16 07:15:42 -04:00
} : {
testInfo : TestInfo
folderSetupFn ? : ( projectDirName : string ) = > Promise < void >
2024-08-16 15:24:36 -04:00
cleanProjectDir? : boolean
2024-08-16 07:15:42 -04:00
} ) {
// create or otherwise clear the folder
const projectDirName = testInfo . outputPath ( 'electron-test-projects-dir' )
try {
2024-08-16 15:24:36 -04:00
if ( fsSync . existsSync ( projectDirName ) && cleanProjectDir ) {
2024-08-16 07:15:42 -04:00
await fsp . rm ( projectDirName , { recursive : true } )
}
} catch ( e ) {
console . error ( e )
}
2024-08-16 15:24:36 -04:00
if ( cleanProjectDir ) {
await fsp . mkdir ( projectDirName )
}
2024-08-16 07:15:42 -04:00
const electronApp = await electron . launch ( {
args : [ '.' , '--no-sandbox' ] ,
env : {
. . . process . env ,
TEST_SETTINGS_FILE_KEY : projectDirName ,
IS_PLAYWRIGHT : 'true' ,
} ,
2024-08-16 12:09:02 -04:00
. . . ( process . env . ELECTRON_OVERRIDE_DIST_PATH
? { executablePath : process.env.ELECTRON_OVERRIDE_DIST_PATH + 'electron' }
: { } ) ,
2024-08-16 07:15:42 -04:00
} )
const context = electronApp . context ( )
const page = await electronApp . firstWindow ( )
context . on ( 'console' , console . log )
page . on ( 'console' , console . log )
2024-08-16 15:24:36 -04:00
if ( cleanProjectDir ) {
const tempSettingsFilePath = join ( projectDirName , SETTINGS_FILE_NAME )
const settingsOverrides = TOML . stringify ( {
. . . TEST_SETTINGS ,
settings : {
app : {
. . . TEST_SETTINGS . app ,
projectDirectory : projectDirName ,
} ,
2024-08-16 07:15:42 -04:00
} ,
2024-08-16 15:24:36 -04:00
} )
await fsp . writeFile ( tempSettingsFilePath , settingsOverrides )
}
2024-08-16 07:15:42 -04:00
await folderSetupFn ? . ( projectDirName )
await setup ( context , page )
return { electronApp , page }
}
2024-08-20 05:34:26 +10:00
export async function isOutOfViewInScrollContainer (
element : Locator ,
container : Locator
) : Promise < boolean > {
const elementBox = await element . boundingBox ( { timeout : 5_000 } )
const containerBox = await container . boundingBox ( { timeout : 5_000 } )
let isOutOfView = false
if ( elementBox && containerBox )
return (
elementBox . y + elementBox . height > containerBox . y + containerBox . height ||
elementBox . y < containerBox . y ||
elementBox . x + elementBox . width > containerBox . x + containerBox . width ||
elementBox . x < containerBox . x
)
return isOutOfView
}
2024-08-22 15:00:13 -05:00
export async function createProjectAndRenameIt ( {
name ,
page ,
} : {
name : string
page : Page
} ) {
await page . getByRole ( 'button' , { name : 'New project' } ) . click ( )
await expect ( page . getByText ( 'Successfully created' ) ) . toBeVisible ( )
await expect ( page . getByText ( 'Successfully created' ) ) . not . toBeVisible ( )
await expect ( page . getByText ( ` project-000 ` ) ) . toBeVisible ( )
await page . getByText ( ` project-000 ` ) . hover ( )
await page . getByText ( ` project-000 ` ) . focus ( )
await page . getByLabel ( 'sketch' ) . first ( ) . click ( )
await page . waitForTimeout ( 100 )
// type the name passed in
await page . keyboard . press ( 'Backspace' )
await page . keyboard . type ( name )
await page . getByLabel ( 'checkmark' ) . last ( ) . click ( )
}