Skip to content

Commit 14f7a5e

Browse files
authored
internal: (studio) supporting before/after snapshots in studio (#33281)
* WIP multiple snapshots iframes for studio * Additional properties for Studio exposure, plumbing through snapshot store * Removing unnecessary class * Better frames container * Updating tests * Another test for panel identifiers * PR feedback * More feedback
1 parent 07a2b68 commit 14f7a5e

File tree

12 files changed

+116
-42
lines changed

12 files changed

+116
-42
lines changed

packages/app/src/runner/ResizablePanels.cy.tsx

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,4 +331,16 @@ describe('<ResizablePanels />', { viewportWidth: 1500 }, () => {
331331
assertWidth('panel3', 1160)
332332
})
333333
})
334+
335+
describe('panel attributes', () => {
336+
it('should have the correct aut-panel identifiers', () => {
337+
cy.mount(() => (
338+
<div class="h-screen">
339+
<ResizablePanels maxTotalWidth={1500} v-slots={slotContents} />
340+
</div>
341+
))
342+
343+
cy.get('[data-cy="aut-panel"]').should('have.id', 'aut-panel')
344+
})
345+
})
334346
})

packages/app/src/runner/ResizablePanels.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
</div>
4545

4646
<div
47+
id="aut-panel"
4748
data-cy="aut-panel"
4849
class="grow h-full bg-gray-100 relative"
4950
:class="{ 'pointer-events-none': panel2IsDragging || panel4IsDragging }"

packages/app/src/runner/SpecRunnerOpenMode.vue

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@
112112
:has-requested-project-access="hasRequestedProjectAccess"
113113
:request-project-access-mutation="requestProjectAccessMutation"
114114
:spec-dirty-data-store="specDirtyDataStore"
115+
:aut-snapshot-store="snapshotStore"
115116
/>
116117
</HideDuringScreenshot>
117118
</template>
@@ -155,6 +156,7 @@ import PromptMoreInfoNeededModal from '../prompt/PromptMoreInfoNeededModal.vue'
155156
import { usePromptStore } from '../store/prompt-store'
156157
import { useUserProjectStatusStore } from '@packages/frontend-shared/src/store/user-project-status-store'
157158
import { useSpecDirtyDataStore } from '../store/spec-dirty-data-store'
159+
import { useSnapshotStore } from './snapshot-store'
158160
159161
// this is used by the StudioPanel to access the AUT URL input
160162
const autUrlSelector = '.aut-url-input'
@@ -170,6 +172,7 @@ const {
170172
171173
const userProjectStatusStore = useUserProjectStatusStore()
172174
const specDirtyDataStore = useSpecDirtyDataStore()
175+
const snapshotStore = useSnapshotStore()
173176
174177
gql`
175178
fragment SpecRunner_Preferences on Query {

packages/app/src/runner/aut-iframe.cy.tsx

Lines changed: 23 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -127,9 +127,15 @@ describe('AutIframe', () => {
127127
const result = autIframe.create()
128128

129129
expect(result).to.have.property('autIframe')
130-
expect(result).to.have.property('autSnapshotIframe')
130+
expect(result).to.have.property('autSnapshotIframes')
131131
expect(autIframe.$iframe).to.equal(result.autIframe)
132-
expect(autIframe.$snapshotIframe).to.equal(result.autSnapshotIframe)
132+
expect(autIframe.$snapshotIframes).to.equal(result.autSnapshotIframes)
133+
expect(result.autSnapshotIframes.length).to.equal(2)
134+
result.autSnapshotIframes.forEach((iframe) => {
135+
expect(iframe.is(':hidden')).to.be.true
136+
expect(iframe.hasClass('aut-snapshot-iframe')).to.be.true
137+
expect(iframe.attr('data-snapshot-index')).to.exist
138+
})
133139
})
134140

135141
it('should create aut iframe with correct attributes', () => {
@@ -143,20 +149,21 @@ describe('AutIframe', () => {
143149

144150
it('should create snapshot iframe with correct attributes', () => {
145151
const result = autIframe.create()
146-
const snapshotIframeElement = result.autSnapshotIframe[0] as HTMLIFrameElement
147152

148-
expect(snapshotIframeElement.id).to.equal('AUT Snapshot: \'Test Project\'')
149-
expect(snapshotIframeElement.title).to.equal('AUT Snapshot: \'Test Project\'')
150-
expect(snapshotIframeElement.className).to.equal('aut-snapshot-iframe')
153+
result.autSnapshotIframes.forEach((iframe) => {
154+
expect(iframe[0].id).to.equal(`AUT Snapshot - ${iframe.data('snapshot-index')}: \'Test Project\'`)
155+
expect(iframe[0].title).to.equal(`AUT Snapshot - ${iframe.data('snapshot-index')}: \'Test Project\'`)
156+
expect(iframe[0].className).to.equal('aut-snapshot-iframe')
157+
})
151158
})
152159

153160
it('verify the snapshot iframe is hidden', () => {
154161
const result = autIframe.create()
155162

156-
result.autSnapshotIframe.appendTo(document.body)
163+
result.autSnapshotIframes[0].appendTo(document.body)
157164
result.autIframe.appendTo(document.body)
158165

159-
expect(result.autSnapshotIframe.is(':hidden')).to.be.true
166+
expect(result.autSnapshotIframes[0].is(':hidden')).to.be.true
160167
expect(result.autIframe.is(':hidden')).to.be.false
161168
})
162169
})
@@ -165,7 +172,7 @@ describe('AutIframe', () => {
165172
it('should remove both aut iframe and snapshot iframe', () => {
166173
const result = autIframe.create()
167174
let autIframeRemoved = false
168-
let snapshotIframeRemoved = false
175+
let snapshotIframesRemoved = [false, false]
169176

170177
// Mock remove methods
171178
result.autIframe.remove = () => {
@@ -174,16 +181,18 @@ describe('AutIframe', () => {
174181
return result.autIframe
175182
}
176183

177-
result.autSnapshotIframe.remove = () => {
178-
snapshotIframeRemoved = true
184+
result.autSnapshotIframes.forEach((snapshotIframe, index) => {
185+
snapshotIframe.remove = () => {
186+
snapshotIframesRemoved[index] = true
179187

180-
return result.autSnapshotIframe
181-
}
188+
return result.autSnapshotIframes[index]
189+
}
190+
})
182191

183192
autIframe.destroy()
184193

185194
expect(autIframeRemoved).to.be.true
186-
expect(snapshotIframeRemoved).to.be.true
195+
expect(snapshotIframesRemoved.every((removed) => removed)).to.be.true
187196
})
188197

189198
it('should throw error when destroy is called without create', () => {

packages/app/src/runner/aut-iframe.ts

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,9 @@ const jQueryRe = /jquery/i
1919
export class AutIframe {
2020
debouncedToggleSelectorPlayground: DebouncedFunc<(isEnabled: any) => void>
2121
$iframe?: JQuery<HTMLIFrameElement>
22-
// the iframe used to display a snapshot of the AUT (currently used to display the studio snapshots)
23-
$snapshotIframe?: JQuery<HTMLIFrameElement>
22+
// the iframes used to display snapshots of the AUT (currently used to display the studio snapshots)
23+
$snapshotIframes?: JQuery<HTMLIFrameElement>[]
24+
2425
_highlightedEl?: Element
2526
private _currentHighlightingId: number = 0
2627

@@ -32,7 +33,7 @@ export class AutIframe {
3233
this.debouncedToggleSelectorPlayground = _.debounce(this.toggleSelectorPlayground, 300)
3334
}
3435

35-
create (): { autIframe: JQuery<HTMLIFrameElement>, autSnapshotIframe: JQuery<HTMLIFrameElement> } {
36+
create (): { autIframe: JQuery<HTMLIFrameElement>, autSnapshotIframes: JQuery<HTMLIFrameElement>[] } {
3637
const $iframe = this.$('<iframe>', {
3738
id: `Your project: '${this.projectName}'`,
3839
title: `Your project: '${this.projectName}'`,
@@ -41,28 +42,36 @@ export class AutIframe {
4142

4243
this.$iframe = $iframe
4344

44-
const $snapshotIframe: JQuery<HTMLIFrameElement> = this.$('<iframe>', {
45-
id: `AUT Snapshot: '${this.projectName}'`,
46-
title: `AUT Snapshot: '${this.projectName}'`,
47-
class: 'aut-snapshot-iframe',
48-
})
45+
// Create two iframes to facilitate before/after snapshot
46+
// rendering with a double buffer.
47+
this.$snapshotIframes = _.times(2, (index) => {
48+
const $snapshotIframe = this.$('<iframe>', {
49+
id: `AUT Snapshot - ${index}: '${this.projectName}'`,
50+
title: `AUT Snapshot - ${index}: '${this.projectName}'`,
51+
class: 'aut-snapshot-iframe',
52+
'data-snapshot-index': index,
53+
})
4954

50-
$snapshotIframe.hide() // Auto-hide the snapshot iframe
51-
this.$snapshotIframe = $snapshotIframe
55+
$snapshotIframe.hide() // Auto-hide the snapshot iframe
56+
57+
return $snapshotIframe
58+
})
5259

5360
return {
5461
autIframe: $iframe,
55-
autSnapshotIframe: $snapshotIframe,
62+
autSnapshotIframes: this.$snapshotIframes,
5663
}
5764
}
5865

5966
destroy () {
60-
if (!this.$iframe || !this.$snapshotIframe) {
67+
if (!this.$iframe || !this.$snapshotIframes) {
6168
throw Error(`Cannot call #remove without first calling #create`)
6269
}
6370

6471
this.$iframe.remove()
65-
this.$snapshotIframe.remove()
72+
this.$snapshotIframes.forEach((iframe) => {
73+
iframe.remove()
74+
})
6675
}
6776

6877
_showInitialBlankPage () {

packages/app/src/runner/event-manager.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -479,11 +479,11 @@ export class EventManager {
479479

480480
initialize ({
481481
$autIframe,
482-
$autSnapshotIframe,
482+
$autSnapshotIframes,
483483
config,
484484
}: {
485485
$autIframe: JQuery<HTMLIFrameElement>
486-
$autSnapshotIframe?: JQuery<HTMLIFrameElement>
486+
$autSnapshotIframes?: JQuery<HTMLIFrameElement>[]
487487
config: Record<string, any>
488488
}) {
489489
performance.mark('initialize-start')
@@ -515,7 +515,7 @@ export class EventManager {
515515

516516
return Cypress.initialize({
517517
$autIframe,
518-
$autSnapshotIframe,
518+
$autSnapshotIframes,
519519
// defining this indicates that the test run should wait for Studio to
520520
// be initialized before running the test
521521
waitForStudio: isStudio ? waitForStudio : undefined,

packages/app/src/runner/index.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -298,17 +298,21 @@ async function runSpecE2E (config, spec: SpecFile) {
298298
// create root for new AUT
299299
const $container = document.createElement('div')
300300

301+
$container.id = 'aut-iframes-container'
301302
$container.classList.add('screenshot-height-container')
302303

303304
$runnerRoot.append($container)
304305

305306
// create new AUT
306307
const autIframe = getAutIframeModel()
307308

308-
const { autIframe: $autIframe, autSnapshotIframe: $autSnapshotIframe } = autIframe.create()
309+
const { autIframe: $autIframe, autSnapshotIframes: $autSnapshotIframes } = autIframe.create()
309310

310311
$autIframe.appendTo($container)
311-
$autSnapshotIframe.appendTo($container)
312+
313+
$autSnapshotIframes.forEach((iframe) => {
314+
iframe.appendTo($container)
315+
})
312316

313317
// Remove the spec bridge iframe
314318
document.querySelectorAll('iframe.spec-bridge-iframe').forEach((el) => {
@@ -333,7 +337,7 @@ async function runSpecE2E (config, spec: SpecFile) {
333337
})
334338

335339
// initialize Cypress (driver) with the AUT!
336-
getEventManager().initialize({ $autIframe, $autSnapshotIframe, config })
340+
getEventManager().initialize({ $autIframe, $autSnapshotIframes, config })
337341
}
338342

339343
/**

packages/app/src/runner/snapshot-store.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,3 +105,5 @@ export const useSnapshotStore = defineStore({
105105
},
106106
},
107107
})
108+
109+
export type SnapshotStore = ReturnType<typeof useSnapshotStore>

packages/app/src/studio/StudioPanel.cy.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@ import type { EventManager } from '../runner/event-manager'
33
import type { UseMutationResponse } from '@urql/vue'
44
import StudioPanel from './StudioPanel.vue'
55
import type { SpecDirtyDataStore } from '../store/spec-dirty-data-store'
6+
import type { SnapshotStore } from '../runner/snapshot-store'
67

78
describe('StudioPanel', () => {
89
it('renders the error panel with a certificate error', () => {
910
const mockEventManager = {} as unknown as EventManager
1011
const mockUserProjectStatusStore = {} as unknown as UserProjectStatusStore
1112
const mockRequestProjectAccessMutation = {} as unknown as UseMutationResponse<any, any>
1213
const mockSpecDirtyDataStore = {} as unknown as SpecDirtyDataStore
14+
const mockAutSnapshotStore = {} as unknown as SnapshotStore
1315

1416
cy.mount(<StudioPanel
1517
isCertError={true}
@@ -22,6 +24,7 @@ describe('StudioPanel', () => {
2224
requestProjectAccessMutation={mockRequestProjectAccessMutation}
2325
autUrlSelector="https://example.com"
2426
specDirtyDataStore={mockSpecDirtyDataStore}
27+
autSnapshotStore={mockAutSnapshotStore}
2528
/>)
2629

2730
cy.findByTestId('studio-error-panel').should('be.visible')

packages/app/src/studio/StudioPanel.vue

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ import type { SpecDirtyDataStore } from '../store/spec-dirty-data-store'
4242
import { useSelectorPlaygroundStore } from '../store/selector-playground-store'
4343
import { getAutIframeModel } from '../runner'
4444
import { closePlayground } from '../runner/selector-playground/utils'
45+
import type { SnapshotStore } from '../runner/snapshot-store'
4546
4647
// Mirrors the ReactDOM.Root type since incorporating those types
4748
// messes up vue typing elsewhere
@@ -68,6 +69,7 @@ const props = defineProps<{
6869
hasRequestedProjectAccess: boolean
6970
requestProjectAccessMutation: UseMutationResponse<any, any>
7071
specDirtyDataStore: SpecDirtyDataStore
72+
autSnapshotStore: SnapshotStore
7173
}>()
7274
7375
interface StudioApp { default: StudioAppDefaultShape }
@@ -140,6 +142,7 @@ const maybeRenderReactComponent = () => {
140142
specDirtyDataStore: props.specDirtyDataStore,
141143
isSelectorPlaygroundOpen: isSelectorPlaygroundOpen.value,
142144
onCloseSelectorPlayground,
145+
autSnapshotStore: props.autSnapshotStore,
143146
})
144147
145148
// Store the react root in a weak map keyed by the container. We do this so that we have a reference
@@ -159,6 +162,7 @@ const maybeRenderReactComponent = () => {
159162
watch(() => props.canAccessStudioAI, maybeRenderReactComponent)
160163
watch(() => props.cloudStudioSessionId, maybeRenderReactComponent)
161164
watch(() => isSelectorPlaygroundOpen.value, maybeRenderReactComponent)
165+
watch(() => props.autSnapshotStore.isSnapshotPinned, maybeRenderReactComponent)
162166
163167
const unmountReactComponent = () => {
164168
if (!ReactStudioPanel.value || !container.value) {

0 commit comments

Comments
 (0)