Compare commits

...

2 Commits

Author SHA1 Message Date
0d7049d90f skip network tests mac 2024-07-08 21:28:07 +10:00
8ebe78c664 Lf94/eco mode save the planet (#2940)
* Trigger shutdown operations after each test

* Idle mode states

* Don't show the reconnect when coming back from tab
2024-07-07 10:10:52 -07:00
7 changed files with 132 additions and 24 deletions

View File

@ -52,7 +52,24 @@ const commonPoints = {
// num2: 19.19, // num2: 19.19,
} }
// Utilities for writing tests that depend on test values test.afterEach(async ({ context, page }, 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)
})
test.beforeEach(async ({ context, page }) => { test.beforeEach(async ({ context, page }) => {
// wait for Vite preview server to be up // wait for Vite preview server to be up
@ -78,7 +95,7 @@ test.beforeEach(async ({ context, page }) => {
await page.emulateMedia({ reducedMotion: 'reduce' }) await page.emulateMedia({ reducedMotion: 'reduce' })
}) })
test.setTimeout(60000) test.setTimeout(120000)
async function doBasicSketch(page: Page, openPanes: string[]) { async function doBasicSketch(page: Page, openPanes: string[]) {
const u = await getUtils(page) const u = await getUtils(page)
@ -6705,6 +6722,11 @@ ${extraLine ? 'const myVar = segLen(seg01, part001)' : ''}`
test.describe('Test network and connection issues', () => { test.describe('Test network and connection issues', () => {
test('simulate network down and network little widget', async ({ page }) => { test('simulate network down and network little widget', async ({ page }) => {
const browserType = page.context().browser()?.browserType().name()
test.skip(
browserType !== 'chromium',
'emulateNetworkConditions only works in chromium'
)
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
@ -6775,6 +6797,11 @@ test.describe('Test network and connection issues', () => {
}) })
test('Engine disconnect & reconnect in sketch mode', async ({ page }) => { test('Engine disconnect & reconnect in sketch mode', async ({ page }) => {
const browserType = page.context().browser()?.browserType().name()
test.skip(
browserType !== 'chromium',
'emulateNetworkConditions only works in chromium'
)
const u = await getUtils(page) const u = await getUtils(page)
await page.setViewportSize({ width: 1200, height: 500 }) await page.setViewportSize({ width: 1200, height: 500 })
const PUR = 400 / 37.5 //pixeltoUnitRatio const PUR = 400 / 37.5 //pixeltoUnitRatio

View File

@ -312,9 +312,9 @@ export async function getUtils(page: Page) {
fullPage: true, fullPage: true,
}) })
const screenshot = await PNG.sync.read(buffer) const screenshot = await PNG.sync.read(buffer)
// most likely related to pixel density but the screenshots for webkit are 2x the size const pixMultiplier: number = await page.evaluate(
// there might be a more robust way of doing this. 'window.devicePixelRatio'
const pixMultiplier = browserType === 'webkit' ? 2 : 1 )
const index = const index =
(screenshot.width * coords.y * pixMultiplier + (screenshot.width * coords.y * pixMultiplier +
coords.x * pixMultiplier) * coords.x * pixMultiplier) *
@ -377,11 +377,13 @@ export async function getUtils(page: Page) {
emulateNetworkConditions: async ( emulateNetworkConditions: async (
networkOptions: Protocol.Network.emulateNetworkConditionsParameters networkOptions: Protocol.Network.emulateNetworkConditionsParameters
) => { ) => {
// Skip on non-Chromium browsers, since we need to use the CDP. if (browserType !== 'chromium') {
test.skip( console.warn('emulateNetworkConditions will not work on this browser')
cdpSession === null, }
'Network emulation is only supported in Chromium' if (cdpSession === null) {
) // Use a fail safe if we can't simulate disconnect (on Safari)
return page.evaluate('window.tearDown()')
}
cdpSession?.send('Network.emulateNetworkConditions', networkOptions) cdpSession?.send('Network.emulateNetworkConditions', networkOptions)
}, },

View File

@ -163,6 +163,8 @@ export const ModelingMachineProvider = ({
store.videoElement?.pause() store.videoElement?.pause()
kclManager.executeCode(true).then(() => { kclManager.executeCode(true).then(() => {
if (engineCommandManager.engineConnection?.freezeFrame) return
store.videoElement?.play() store.videoElement?.play()
}) })
})() })()

View File

@ -8,7 +8,7 @@ import { NetworkHealthState } from 'hooks/useNetworkStatus'
import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp' import { ClientSideScene } from 'clientSideScene/ClientSideSceneComp'
import { butName } from 'lib/cameraControls' import { butName } from 'lib/cameraControls'
import { sendSelectEventToEngine } from 'lib/selections' import { sendSelectEventToEngine } from 'lib/selections'
import { kclManager } from 'lib/singletons' import { kclManager, engineCommandManager } from 'lib/singletons'
export const Stream = () => { export const Stream = () => {
const [isLoading, setIsLoading] = useState(true) const [isLoading, setIsLoading] = useState(true)
@ -18,6 +18,7 @@ export const Stream = () => {
const { settings } = useSettingsAuthContext() const { settings } = useSettingsAuthContext()
const { state, send, context } = useModelingContext() const { state, send, context } = useModelingContext()
const { overallState } = useNetworkContext() const { overallState } = useNetworkContext()
const [isFreezeFrame, setIsFreezeFrame] = useState(false)
const isNetworkOkay = const isNetworkOkay =
overallState === NetworkHealthState.Ok || overallState === NetworkHealthState.Ok ||
@ -49,14 +50,69 @@ export const Stream = () => {
globalThis?.window?.document?.addEventListener('paste', handlePaste, { globalThis?.window?.document?.addEventListener('paste', handlePaste, {
capture: true, capture: true,
}) })
return () =>
// Teardown everything if we go hidden or reconnect
if (globalThis?.window?.document) {
globalThis.window.document.onvisibilitychange = () => {
if (globalThis.window.document.visibilityState === 'hidden') {
videoRef.current?.pause()
setIsFreezeFrame(true)
window.requestAnimationFrame(() => {
engineCommandManager.engineConnection?.tearDown({ freeze: true })
})
} else {
engineCommandManager.engineConnection?.connect(true)
}
}
}
const IDLE_TIME_MS = 1000 * 20
let timeoutIdIdle: ReturnType<typeof setTimeout> | undefined = undefined
const onIdle = () => {
videoRef.current?.pause()
setIsFreezeFrame(true)
kclManager.isFirstRender = true
setIsFirstRender(true)
// Give video time to pause
window.requestAnimationFrame(() => {
engineCommandManager.engineConnection?.tearDown({ freeze: true })
})
}
const onAnyInput = () => {
if (!engineCommandManager.engineConnection?.isReady()) {
engineCommandManager.engineConnection?.connect(true)
}
clearTimeout(timeoutIdIdle)
timeoutIdIdle = setTimeout(onIdle, IDLE_TIME_MS)
}
globalThis?.window?.document?.addEventListener('keydown', onAnyInput)
globalThis?.window?.document?.addEventListener('mousemove', onAnyInput)
globalThis?.window?.document?.addEventListener('mousedown', onAnyInput)
globalThis?.window?.document?.addEventListener('scroll', onAnyInput)
globalThis?.window?.document?.addEventListener('touchstart', onAnyInput)
timeoutIdIdle = setTimeout(onIdle, IDLE_TIME_MS)
return () => {
globalThis?.window?.document?.removeEventListener('paste', handlePaste, { globalThis?.window?.document?.removeEventListener('paste', handlePaste, {
capture: true, capture: true,
}) })
globalThis?.window?.document?.removeEventListener('keydown', onAnyInput)
globalThis?.window?.document?.removeEventListener('mousemove', onAnyInput)
globalThis?.window?.document?.removeEventListener('mousedown', onAnyInput)
globalThis?.window?.document?.removeEventListener('scroll', onAnyInput)
globalThis?.window?.document?.removeEventListener(
'touchstart',
onAnyInput
)
}
}, []) }, [])
useEffect(() => { useEffect(() => {
setIsFirstRender(kclManager.isFirstRender) setIsFirstRender(kclManager.isFirstRender)
if (!kclManager.isFirstRender) videoRef.current?.play()
}, [kclManager.isFirstRender]) }, [kclManager.isFirstRender])
useEffect(() => { useEffect(() => {
@ -67,7 +123,10 @@ export const Stream = () => {
return return
if (!videoRef.current) return if (!videoRef.current) return
if (!context.store?.mediaStream) return if (!context.store?.mediaStream) return
// Do not immediately play the stream!
videoRef.current.srcObject = context.store.mediaStream videoRef.current.srcObject = context.store.mediaStream
videoRef.current.pause()
send({ send({
type: 'Set context', type: 'Set context',
@ -172,17 +231,12 @@ export const Stream = () => {
<ClientSideScene <ClientSideScene
cameraControls={settings.context.modeling.mouseControls.current} cameraControls={settings.context.modeling.mouseControls.current}
/> />
{!isNetworkOkay && !isLoading && ( {(!isNetworkOkay || isLoading || isFirstRender) && !isFreezeFrame && (
<div className="text-center absolute inset-0"> <div className="text-center absolute inset-0">
<Loading> <Loading>
<span data-testid="loading-stream">Stream disconnected...</span> {!isNetworkOkay && !isLoading ? (
</Loading> <span data-testid="loading-stream">Stream disconnected...</span>
</div> ) : !isLoading && isFirstRender ? (
)}
{(isLoading || isFirstRender) && (
<div className="text-center absolute inset-0">
<Loading>
{!isLoading && isFirstRender ? (
<span data-testid="loading-stream">Building scene...</span> <span data-testid="loading-stream">Building scene...</span>
) : ( ) : (
<span data-testid="loading-stream">Loading stream...</span> <span data-testid="loading-stream">Loading stream...</span>

View File

@ -300,6 +300,7 @@ class EngineConnection extends EventTarget {
pc?: RTCPeerConnection pc?: RTCPeerConnection
unreliableDataChannel?: RTCDataChannel unreliableDataChannel?: RTCDataChannel
mediaStream?: MediaStream mediaStream?: MediaStream
freezeFrame: boolean = false
private _state: EngineConnectionState = { private _state: EngineConnectionState = {
type: EngineConnectionStateType.Fresh, type: EngineConnectionStateType.Fresh,
@ -365,7 +366,11 @@ class EngineConnection extends EventTarget {
this.pingPongSpan = { ping: undefined, pong: undefined } this.pingPongSpan = { ping: undefined, pong: undefined }
// Without an interval ping, our connection will timeout. // Without an interval ping, our connection will timeout.
// If this.freezeFrame is true we skip this logic so only reconnect
// happens on mouse move
setInterval(() => { setInterval(() => {
if (this.freezeFrame) return
switch (this.state.type as EngineConnectionStateType) { switch (this.state.type as EngineConnectionStateType) {
case EngineConnectionStateType.ConnectionEstablished: case EngineConnectionStateType.ConnectionEstablished:
// If there was no reply to the last ping, report a timeout. // If there was no reply to the last ping, report a timeout.
@ -426,7 +431,8 @@ class EngineConnection extends EventTarget {
return this.state.type === EngineConnectionStateType.ConnectionEstablished return this.state.type === EngineConnectionStateType.ConnectionEstablished
} }
tearDown() { tearDown(opts?: { freeze: boolean }) {
this.freezeFrame = opts?.freeze ?? false
this.disconnectAll() this.disconnectAll()
this.state = { this.state = {
type: EngineConnectionStateType.Disconnecting, type: EngineConnectionStateType.Disconnecting,
@ -996,6 +1002,9 @@ class EngineConnection extends EventTarget {
this.pc?.connectionState === 'closed' && this.pc?.connectionState === 'closed' &&
this.unreliableDataChannel?.readyState === 'closed' this.unreliableDataChannel?.readyState === 'closed'
if (allClosed) { if (allClosed) {
// Do not notify the rest of the program that we have cut off anything.
if (this.freezeFrame) return
this.state = { type: EngineConnectionStateType.Disconnected } this.state = { type: EngineConnectionStateType.Disconnected }
} }
} }
@ -1619,7 +1628,15 @@ export class EngineCommandManager extends EventTarget {
} }
} }
tearDown() { tearDown() {
this.engineConnection?.tearDown() if (this.engineConnection) {
this.engineConnection?.tearDown()
// Our window.tearDown assignment causes this case to happen which is
// only really for tests.
// @ts-ignore
} else if (this.engineCommandManager?.engineConnection) {
// @ts-ignore
this.engineCommandManager?.engineConnection?.tearDown()
}
} }
async startNewSession() { async startNewSession() {
this.lastArtifactMap = this.artifactMap this.lastArtifactMap = this.artifactMap

View File

@ -9,6 +9,10 @@ export const codeManager = new CodeManager()
export const engineCommandManager = new EngineCommandManager() export const engineCommandManager = new EngineCommandManager()
// Accessible for tests mostly
// @ts-ignore
window.tearDown = engineCommandManager.tearDown
// This needs to be after codeManager is created. // This needs to be after codeManager is created.
export const kclManager = new KclManager(engineCommandManager) export const kclManager = new KclManager(engineCommandManager)
kclManager.isFirstRender = true kclManager.isFirstRender = true

View File

@ -1051,7 +1051,9 @@ export const modelingMachine = createMachine(
type: 'start_path', type: 'start_path',
}, },
}) })
store.videoElement?.play() if (!engineCommandManager.engineConnection?.freezeFrame) {
store.videoElement?.play()
}
if (updatedAst?.selections) { if (updatedAst?.selections) {
editorManager.selectRange(updatedAst?.selections) editorManager.selectRange(updatedAst?.selections)
} }