Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: wire up Cypress Studio #23413

Merged
merged 38 commits into from
Aug 23, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
66387f0
wip
lmiller1990 Aug 4, 2022
1131c73
wip
lmiller1990 Aug 5, 2022
0404134
Merge remote-tracking branch 'origin/develop' into lmiller/spike-studio
lmiller1990 Aug 8, 2022
a61e667
wip - spike
lmiller1990 Aug 8, 2022
0e9863f
merge in develop [skip ci]
lmiller1990 Aug 17, 2022
ab31dcc
more wip [skip ci]
lmiller1990 Aug 17, 2022
87fb624
update style
lmiller1990 Aug 17, 2022
ba24e88
fix ts
lmiller1990 Aug 17, 2022
42043a1
Merge remote-tracking branch 'origin/develop' into lmiller/spike-studio
lmiller1990 Aug 18, 2022
09d2786
move types around
lmiller1990 Aug 18, 2022
c3506a6
extract types
lmiller1990 Aug 18, 2022
db23066
lint
lmiller1990 Aug 18, 2022
a0438da
Merge remote-tracking branch 'origin/develop' into lmiller/spike-studio
lmiller1990 Aug 19, 2022
ca309ea
fixing tests
lmiller1990 Aug 19, 2022
5b07449
fix component test
lmiller1990 Aug 19, 2022
770e033
skip some tests
lmiller1990 Aug 19, 2022
a7b714e
do not error on experimentalStudio flag
lmiller1990 Aug 19, 2022
fc66414
add studio controls placeholder
lmiller1990 Aug 19, 2022
55e3a31
fixing tests
lmiller1990 Aug 19, 2022
08d023f
revert
lmiller1990 Aug 19, 2022
62a4f51
revert changes
lmiller1990 Aug 19, 2022
be9a801
rename store
lmiller1990 Aug 19, 2022
36cc510
rename file
lmiller1990 Aug 19, 2022
dd435fa
rename method
lmiller1990 Aug 19, 2022
568a7f6
remove comment
lmiller1990 Aug 19, 2022
40327d8
refactor
lmiller1990 Aug 19, 2022
b0299c3
correctly feature flag studio
lmiller1990 Aug 19, 2022
219fc41
Merge remote-tracking branch 'origin/develop' into lmiller/spike-studio
lmiller1990 Aug 22, 2022
fd0ee2b
simplify code
lmiller1990 Aug 22, 2022
fa5c9eb
simplify code
lmiller1990 Aug 22, 2022
4fdb73f
lift check into useEventManager
lmiller1990 Aug 22, 2022
9a4e5fd
correctly hide create studio prompt based on flag;
lmiller1990 Aug 22, 2022
65eeae3
remove superfulous css
lmiller1990 Aug 22, 2022
5f3bd1f
rename variables
lmiller1990 Aug 22, 2022
82b43c4
fix bugs
lmiller1990 Aug 23, 2022
4af852a
wip
lmiller1990 Aug 23, 2022
9c24014
unskip tests
lmiller1990 Aug 23, 2022
c7d66f5
unskip more tests
lmiller1990 Aug 23, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 0 additions & 4 deletions packages/app/cypress/component/support/ctSupport.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import { AutIframe } from '../../../src/runner/aut-iframe'
import { EventManager } from '../../../src/runner/event-manager'
import type { Socket } from '@packages/socket/lib/browser'

class StudioRecorderMock {}

export const StubWebsocket = new Proxy<Socket>(Object.create(null), {
get: (obj, prop) => {
throw Error(`Cannot access ${String(prop)} on StubWebsocket!`)
Expand All @@ -29,7 +27,6 @@ export const createEventManager = () => {
// @ts-ignore
null, // MobX, also not needed in Vue CT tests,
null, // selectorPlaygroundModel,
StudioRecorderMock, // needs to be a valid class
StubWebsocket,
)
}
Expand All @@ -53,6 +50,5 @@ export const createTestAutIframe = (eventManager = createEventManager()) => {
// dom - imports driver which causes problems
// so just stubbing it out for now
mockDom,
eventManager.studioRecorder,
)
}
2 changes: 1 addition & 1 deletion packages/app/cypress/e2e/cypress-in-cypress-e2e.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ describe('Cypress In Cypress E2E', { viewportWidth: 1500, defaultCommandTimeout:
})

it('shows a compilation error with a malformed spec', { viewportHeight: 596, viewportWidth: 1000 }, () => {
const expectedAutHeight = 500 // based on explicitly setting viewport in this test to 596
const expectedAutHeight = 456 // based on explicitly setting viewport in this test to 596
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I changed the <a> for the AUT URL to be an <input> so we can type into it to navigate to a URL for Cypress Studio, and for some reason the wrapping break points for flex-wrap is slightly different. I have no idea why and was quite stuck on this, but for all intensive purposes the UI is the same, except things wrap in the AUT header slightly earlier if you have your Cypress window really small.

We can probably revisit this in the final pass if needed, but I don't think it makes any difference.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change <a> to <input> for the Studio URL input - this caused the breakpoint for flex: flex-wrap to change slightly. Seems like it's not a big deal.


cy.visitApp()

Expand Down
2 changes: 1 addition & 1 deletion packages/app/cypress/e2e/cypress-in-cypress.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,7 +243,7 @@ describe('Cypress in Cypress', { viewportWidth: 1500, defaultCommandTimeout: 100
cy.visitApp()
cy.contains('dom-content.spec').click()

cy.contains('http://localhost:4455/cypress/e2e/dom-content.html').should('be.visible')
cy.findByTestId('aut-url-input').invoke('val').should('contain', 'http://localhost:4455/cypress/e2e/dom-content.html')
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bit more specific about what's going on, just a general improvement.

cy.findByLabelText('Stats').should('not.exist')
cy.findByTestId('specs-list-panel').should('not.be.visible')
cy.findByTestId('reporter-panel').should('not.be.visible')
Expand Down
10 changes: 6 additions & 4 deletions packages/app/cypress/e2e/reporter_header.cy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,20 @@ describe('Reporter Header', () => {
it('filters the list of specs when searching for specs', () => {
cy.get('body').type('f')

cy.get('input').type('dom', { force: true })
cy.findByTestId('specs-list-panel').within(() => {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again more specific, we've got another <input> now for the Studio URL to visit for the test, so we need to make this more specific.

cy.get('input').as('searchInput').type('dom', { force: true })
})

cy.get('[data-cy="spec-file-item"]').should('have.length', 3)
.should('contain', 'dom-content.spec')

cy.get('input').clear()
cy.get('@searchInput').clear()

cy.get('[data-cy="spec-file-item"]').should('have.length', 3)

cy.get('input').type('asdf', { force: true })
cy.get('@searchInput').type('asdf', { force: true })

cy.get('[data-cy="spec-file-item"]').should('have.length', 0)
cy.findByTestId('spec-file-item').should('have.length', 0)
})
})

Expand Down
5 changes: 2 additions & 3 deletions packages/app/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,13 @@ import 'virtual:windi.css'
import urql from '@urql/vue'
import App from './App.vue'
import { makeUrqlClient } from '@packages/frontend-shared/src/graphql/urqlClient'
import { decodeBase64Unicode } from '@packages/frontend-shared/src/utils/base64'
import { createI18n } from '@cy/i18n'
import { createRouter } from './router/router'
import { injectBundle } from './runner/injectBundle'
import { createPinia } from './store'
import Toast, { POSITION } from 'vue-toastification'
import 'vue-toastification/dist/index.css'
import { createWebsocket } from './runner'
import { createWebsocket, getRunnerConfigFromWindow } from './runner'

// set a global so we can run
// conditional code in the vite branch
Expand All @@ -21,7 +20,7 @@ window.__vite__ = true

const app = createApp(App)

const config = JSON.parse(decodeBase64Unicode(window.__CYPRESS_CONFIG__.base64Config)) as Cypress.Config
const config = getRunnerConfigFromWindow()

const ws = createWebsocket(config.socketIoRoute)

Expand Down
2 changes: 1 addition & 1 deletion packages/app/src/runner/SpecRunnerHeaderOpenMode.cy.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ describe('SpecRunnerHeaderOpenMode', { viewportHeight: 500 }, () => {
},
})

cy.contains(url).should('exist').should('have.attr', 'href', url)
cy.findByTestId('aut-url-input').invoke('val').should('contain', url)
cy.percySnapshot()
})

Expand Down
39 changes: 34 additions & 5 deletions packages/app/src/runner/SpecRunnerHeaderOpenMode.vue
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,16 @@
>
<i-cy-crosshairs_x16 :class="[selectorPlaygroundStore.show ? 'icon-dark-indigo-500' : 'icon-dark-gray-500']" />
</Button>
<a
<input
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is the <input> I was talking about.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why did we need to change this to an input? I think I missed the reason

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is here: #23413 (comment)

I think it was an input in Cypress 9, which I always didn't really love - I'm hoping when we clean up the UX for this, this will only render an input when it is actually editable in studio mode, and will render a plain old link the rest of the time.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess <input> as a link is not really the best a11y. We could do a fancy switcharoo depending if studio is active? Or, hack <a> to be editable (but this seems not ideal).

I had a ton of problems with tailwind doing weird things - for some reason the sizing changes between <a> and <input> and messed up the flow of the flex-wrap, even though tailwind claims to do a full CSS reset. Probably need to jam with a CSS-minded individual to sort this out.

target="_blank"
:href="autStore.url"
class="mr-12px leading-normal max-w-100% text-indigo-500 self-center hocus-link-default truncate"
:value="autUrl"
data-cy="aut-url-input"
class="mr-12px leading-normal max-w-100% text-indigo-500 self-center hocus-link-default truncate flex flex-grow"
@input="setStudioUrl"
@click="openInNewTab"
>
{{ autStore.url }}
</a>
</div>

<div
v-else
class="flex-grow"
Expand Down Expand Up @@ -129,6 +131,8 @@
:event-manager="eventManager"
/>

<StudioControls v-if="studioStore.isActive" />

<Alert
v-model="showAlert"
status="success"
Expand Down Expand Up @@ -160,11 +164,13 @@ import SelectorPlayground from './selector-playground/SelectorPlayground.vue'
import ExternalLink from '@packages/frontend-shared/src/gql-components/ExternalLink.vue'
import Alert from '@packages/frontend-shared/src/components/Alert.vue'
import Button from '@packages/frontend-shared/src/components/Button.vue'
import StudioControls from './StudioControls.vue'
import VerticalBrowserListItems from '@packages/frontend-shared/src/gql-components/topnav/VerticalBrowserListItems.vue'
import InlineCodeFragment from '@packages/frontend-shared/src/components/InlineCodeFragment.vue'
import SpecRunnerDropdown from './SpecRunnerDropdown.vue'
import { allBrowsersIcons } from '@packages/frontend-shared/src/assets/browserLogos'
import BookIcon from '~icons/cy/book_x16'
import { useStudioStore } from '../store/studio-store'

gql`
fragment SpecRunnerHeader on CurrentProject {
Expand All @@ -189,6 +195,8 @@ const specStore = useSpecStore()

const route = useRoute()

const studioStore = useStudioStore()

const props = defineProps<{
gql: SpecRunnerHeaderFragment
eventManager: EventManager
Expand All @@ -209,6 +217,14 @@ const displayScale = computed(() => {
return autStore.scale < 1 ? `${Math.round(autStore.scale * 100) }%` : 0
})

const autUrl = computed(() => {
if (studioStore.isActive && studioStore.url) {
return studioStore.url
}

return autStore.url
})

const selectorPlaygroundStore = useSelectorPlaygroundStore()

const togglePlayground = () => _togglePlayground(autIframe)
Expand All @@ -220,4 +236,17 @@ const activeSpecPath = specStore.activeSpec?.absolute

const isDisabled = computed(() => autStore.isRunning || autStore.isLoading)

function setStudioUrl (event: Event) {
const url = (event.currentTarget as HTMLInputElement).value

studioStore.setUrl(url)
}

function openInNewTab () {
if (!autStore.url || studioStore.isActive) {
return
}

window.open(autStore.url, '_blank')?.focus()
}
</script>
118 changes: 118 additions & 0 deletions packages/app/src/runner/StudioControls.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
<template>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not final, just some placeholder until #23337 lands

<div v-if="!studioStore.url && studioStore.isActive">
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are missing tests for this component.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, thanks for calling this out -- DM'd you, gonna merge this to the feature branch and add a bunch of tests here: #23461

<b>Please enter a valid URL to visit.</b>
</div>

<button
v-if="studioStore.url && studioStore.isActive"
@click="visitUrl"
>
Go ➜
</button>

<div>
<b>Studio Beta</b>
</div>

<button
:disabled="studioStore.isLoading"
@click="handleShowCommands"
>
Available Commands
</button>

<a
href="https://on.cypress.io/studio-beta"
target="_blank"
>Give feedback</a>

<div>
<!-- these are the buttons that do the things -->
<button
:disabled="studioStore.isLoading"
@click="handleClose"
>
Close Studio
</button>

<button
:disabled="studioStore.isLoading"
@click="handleRestart"
>
Restart
</button>

<button
:disabled="studioStore.isLoading || studioStore.isEmpty"
@click="handleCopyCommands"
>
Copy Commands
</button>

<button
:disabled="studioStore.isLoading || studioStore.isEmpty"
@click="handleSaveCommands"
>
Save Commands
</button>

<input
v-if="studioStore.saveModalIsOpen"
v-model="testName"
style="border: 1px solid black"
>
<button
:disabled="!testName"
@click="handleSave"
>
Save Test
</button>
</div>
</template>

<script setup lang="ts">
import { ref } from 'vue'
import { getEventManager } from '.'
import { useStudioStore } from '../store/studio-store'

const studioStore = useStudioStore()

function handleShowCommands () {
// TODO: Show modal with available commands'
}

function handleSave () {
studioStore.save(testName.value)
}

const eventManager = getEventManager()

const testName = ref('')

function handleClose () {
eventManager.emit('studio:cancel', undefined)
}

function handleRestart () {
studioStore.reset()
eventManager.emit('restart', undefined)
}

function handleCopyCommands () {
eventManager.emit('studio:copy:to:clipboard', () => {
// optional callback - do we need this?
})
}

function handleSaveCommands () {
studioStore.startSave()
}

function visitUrl () {
if (!studioStore.url) {
throw Error('Cannot visit blank url')
}

studioStore.visitUrl(studioStore.url)
}
</script>
26 changes: 17 additions & 9 deletions packages/app/src/runner/aut-iframe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { logger } from './logger'
import _ from 'lodash'
/* eslint-disable no-duplicate-imports */
import type { DebouncedFunc } from 'lodash'
import { useStudioStore } from '../store/studio-store'

// JQuery bundled w/ Cypress
type $CypressJQuery = any
Expand All @@ -18,7 +19,6 @@ export class AutIframe {
private eventManager: any,
private $: $CypressJQuery,
private dom: any,
private studioRecorder: any,
) {
this.debouncedToggleSelectorPlayground = _.debounce(this.toggleSelectorPlayground, 300)
}
Expand Down Expand Up @@ -76,7 +76,7 @@ export class AutIframe {
}

_body () {
return this._contents()?.find('body')
return this._contents()?.find('body') as unknown as JQuery<HTMLBodyElement>
}

detachDom = () => {
Expand Down Expand Up @@ -481,15 +481,23 @@ export class AutIframe {
})
}

startStudio = () => {
if (this.studioRecorder.isLoading) {
this.studioRecorder.start(this._body()?.[0])
}
startStudio () {
const studioStore = useStudioStore()

studioStore.start(this._body()?.[0])
}

reattachStudio = () => {
if (this.studioRecorder.isActive) {
this.studioRecorder.attachListeners(this._body()?.[0])
reattachStudio () {
const studioStore = useStudioStore()

if (studioStore.isActive) {
const body = this._body()?.[0]

if (!body) {
throw Error(`Cannot reattach Studio without the HTMLBodyElement for the app`)
}

studioStore.attachListeners(body)
}
}
}
Loading