Merge branch '10.0-release' of https://github.com/cypress-io/cypress into tbiethman/develop-into-10-with-electron

This commit is contained in:
Tyler Biethman
2022-05-16 17:09:05 -05:00
32 changed files with 303 additions and 299 deletions

View File

@@ -45,13 +45,11 @@ describe('runner/cypress sessions.ui.spec', {
validateSessionsInstrumentPanel(['blank_session'])
cy.get('.command-name-session')
.first()
.within(() => {
cy.get('i.command-message-indicator-successful')
.siblings()
.should('contain', '(new) blank_session')
cy.get('.command-name-session').contains('blank_session')
validateCreateNewSessionGroup()
})
@@ -70,14 +68,11 @@ describe('runner/cypress sessions.ui.spec', {
validateSessionsInstrumentPanel(['blank_session'])
cy.get('.command-name-session')
.first()
.within(() => {
cy.get('i.command-message-indicator-successful')
.siblings()
.should('contain', '(new) blank_session')
cy.get('.command-name-session').contains('blank_session')
validateCreateNewSessionGroup()
cy.contains('Validate Session: valid')
@@ -106,14 +101,11 @@ describe('runner/cypress sessions.ui.spec', {
validateSessionsInstrumentPanel(['blank_session'])
cy.get('.command-name-session')
.first()
.within(() => {
cy.get('i.command-message-indicator-successful')
.siblings()
.should('contain', '(new) blank_session')
cy.get('.command-name-session').contains('blank_session')
validateCreateNewSessionGroup()
cy.contains('Validate Session: invalid')
@@ -151,13 +143,10 @@ describe('runner/cypress sessions.ui.spec', {
validateSessionsInstrumentPanel(['user1'])
cy.get('.command-name-session')
.first()
.within(() => {
cy.get('i.command-message-indicator-pending')
.siblings().should('contain', '(saved) user1')
cy.get('.command-name-session').contains('user1')
cy.contains('Restore Saved Session')
.closest('.command')
.contains('Clear Page')
@@ -208,13 +197,10 @@ describe('runner/cypress sessions.ui.spec', {
validateSessionsInstrumentPanel(['user1'])
cy.get('.command-name-session')
.first()
.within(() => {
cy.get('i.command-message-indicator-bad')
.siblings().should('contain', '(recreated) user1')
cy.get('.command-name-session').contains('user1')
cy.contains('Restore Saved Session')
.closest('.command')
.contains('Clear Page')
@@ -272,13 +258,10 @@ describe('runner/cypress sessions.ui.spec', {
validateSessionsInstrumentPanel(['user1'])
cy.get('.command-name-session')
.first()
.within(() => {
cy.get('i.command-message-indicator-bad')
.siblings().should('contain', '(recreated) user1')
cy.get('.command-name-session').contains('user1')
cy.contains('Restore Saved Session')
.closest('.command')
.contains('Clear Page')

View File

@@ -99,7 +99,7 @@ describe('App: Settings', () => {
cy.waitForSpecToFinish()
// Wait for the test to pass, so the test is completed
cy.get('.passed > .num').should('contain', 1)
cy.get(`[href='#/settings']`).click()
cy.get('[href="#/settings"]').click()
cy.contains('Dashboard Settings').click()
// Assert the data is not there before it arrives
cy.contains('Record Key').should('not.exist')
@@ -114,18 +114,18 @@ describe('App: Settings', () => {
o.sinon.spy(ctx.actions.auth, 'logout')
})
cy.get(`[href='#/settings']`).click()
cy.get('[href="#/settings"]').click()
cy.contains('Dashboard Settings').click()
cy.contains('Record Key').should('exist')
cy.get(`[href='#/runs']`).click()
cy.get('[data-cy="user-avatar-title"]').click()
cy.get('[href="#/runs"]').click()
cy.findByTestId('user-avatar-title').click()
cy.findByRole('button', { name: 'Log Out' }).click()
cy.withRetryableCtx((ctx, o) => {
expect(ctx.actions.auth.logout).to.have.been.calledOnce
})
cy.get(`[href='#/settings']`).click()
cy.get('[href="#/settings"]').click()
cy.contains('Dashboard Settings').click()
cy.contains('Record Key').should('not.exist')
})

View File

@@ -1,6 +1,50 @@
import type { SinonStub } from 'sinon'
describe('Sidebar Navigation', () => {
context('accessibility', () => {
beforeEach(() => {
cy.scaffoldProject('todos')
cy.openProject('todos')
cy.startAppServer()
cy.visitApp()
cy.contains('todos')
})
it('can tab through navigation', () => {
cy.get('body').focus()
.tab().should('have.attr', 'data-cy', 'toggle-sidebar').should('have.prop', 'tagName', 'BUTTON')
.tab().should('have.attr', 'data-cy', 'sidebar-header').should('have.attr', 'role', 'button')
.tab().should('have.attr', 'href', '#/specs').should('have.prop', 'tagName', 'A')
.tab().should('have.attr', 'href', '#/runs').should('have.prop', 'tagName', 'A')
.tab().should('have.attr', 'href', '#/settings').should('have.prop', 'tagName', 'A')
.tab().should('have.attr', 'data-cy', 'keyboard-modal-trigger').should('have.prop', 'tagName', 'BUTTON')
})
it('has no axe violations', () => {
cy.injectAxe()
cy.checkA11y('[data-cy="sidebar"]')
})
it('has appropriate aria attributes', () => {
cy.findByTestId('toggle-sidebar')
.should('have.attr', 'aria-controls', 'sidebar')
.should('have.attr', 'aria-expanded', 'true')
.should('have.attr', 'aria-label', 'Collapse sidebar')
.click()
.should('have.attr', 'aria-expanded', 'false')
.should('have.attr', 'aria-label', 'Expand sidebar')
cy.findByTestId('keyboard-modal-trigger')
.should('have.attr', 'aria-label', 'Keyboard Shortcuts')
cy.get('nav')
.should('have.attr', 'aria-label', 'Pages')
cy.findByTestId('sidebar-header')
.should('have.attr', 'aria-label', 'todos - Choose a testing type')
})
})
context('as e2e testing type with localSettings', () => {
it('use saved state for nav size', () => {
cy.withCtx(async (ctx) => {
@@ -16,7 +60,7 @@ describe('Sidebar Navigation', () => {
cy.get('.toggle-specs-text').click()
cy.get('[data-cy="reporter-panel"]').invoke('outerWidth').then(($initialWidth) => {
cy.findByTestId('reporter-panel').invoke('outerWidth').then(($initialWidth) => {
expect($initialWidth).eq(100)
})
})
@@ -32,66 +76,45 @@ describe('Sidebar Navigation', () => {
})
it('expands the left nav bar by default', () => {
cy.findByLabelText('Sidebar').closest('[aria-expanded]').should('have.attr', 'aria-expanded', 'true')
cy.findByTestId('sidebar').should('have.css', 'width', '248px') // assert width to ensure transition has finished
cy.percySnapshot()
})
it('highlights indicator on hover showing you can click to expand', () => {
const navIndicatorSelector = '[data-testid=sidebar-nav-indicator]'
const navExpansionToggleSelector = '[aria-label="toggle navigation"]'
const navIndicatorSelector = '[data-cy=sidebar-nav-indicator]'
cy.get(navIndicatorSelector)
.should('not.be.visible')
.get(navExpansionToggleSelector)
.realHover()
.get(navIndicatorSelector)
.should('be.visible')
.percySnapshot()
cy.get(navIndicatorSelector).should('not.be.visible')
cy.findByTestId('toggle-sidebar').realHover()
cy.get(navIndicatorSelector).should('be.visible')
cy.percySnapshot()
})
it('closes the left nav bar when clicking the expand button (if expanded)', () => {
cy.findByLabelText('Sidebar').closest('[aria-expanded]').should('have.attr', 'aria-expanded', 'true')
cy.findAllByText('todos').eq(1).as('title')
cy.get('@title').should('be.visible')
cy.findByLabelText('toggle navigation', {
selector: 'button',
}).click()
cy.findByLabelText('Sidebar').closest('[aria-expanded]').should('have.attr', 'aria-expanded', 'false')
cy.get('@title').should('not.be.visible')
cy.findByTestId('sidebar').contains('todos').should('be.visible')
cy.findByTestId('toggle-sidebar').click()
cy.findByTestId('sidebar').contains('todos').should('not.be.visible')
cy.percySnapshot()
})
it('closes the left nav bar when clicking the expand button and persist the state if browser is refreshed', () => {
cy.findByLabelText('Sidebar').closest('[aria-expanded]').should('have.attr', 'aria-expanded', 'true')
cy.findAllByText('todos').eq(1).as('title')
cy.get('@title').should('be.visible')
cy.findByTestId('sidebar').contains('todos').should('be.visible')
cy.findByTestId('toggle-sidebar').click()
cy.findByLabelText('toggle navigation', {
selector: 'button',
}).click()
cy.findByLabelText('Sidebar').closest('[aria-expanded]').should('have.attr', 'aria-expanded', 'false')
cy.get('@title').should('not.be.visible')
cy.findByTestId('sidebar').contains('todos').should('not.be.visible')
cy.reload()
cy.findByLabelText('Sidebar').closest('[aria-expanded]').should('have.attr', 'aria-expanded', 'false')
cy.findAllByText('todos').should('not.be.visible')
cy.findByTestId('sidebar').contains('todos').should('not.be.visible')
cy.percySnapshot()
})
it('has unlabeled menu item that shows the keyboard shortcuts modal (unexpanded state)', () => {
cy.findByLabelText('toggle navigation', {
selector: 'button',
}).click()
it('has menu item that shows the keyboard shortcuts modal (unexpanded state)', () => {
cy.findByTestId('toggle-sidebar').click()
cy.findByTestId('sidebar').should('have.css', 'width', '64px') // assert width to ensure transition has finished
cy.findByLabelText('Sidebar').closest('[aria-expanded]').should('have.attr', 'aria-expanded', 'false')
cy.get('[data-cy="keyboard-shortcuts"]').should('be.visible')
cy.get('[data-cy="keyboard-shortcuts"]').click()
cy.findByTestId('keyboard-modal-trigger').should('be.visible').click()
cy.contains('h2', 'Keyboard Shortcuts').should('be.visible')
cy.get('li p').contains('Re-run tests').should('be.visible')
cy.get('li p').contains('Stop tests').should('be.visible')
@@ -106,52 +129,38 @@ describe('Sidebar Navigation', () => {
})
it('shows a tooltip when hovering over menu item', () => {
cy.findByLabelText('toggle navigation', {
selector: 'button',
}).click()
cy.findByTestId('toggle-sidebar').click()
cy.findByTestId('sidebar').should('have.css', 'width', '64px') // assert width to ensure transition has finished
cy.findByLabelText('Sidebar').closest('[aria-expanded]').should('have.attr', 'aria-expanded', 'false')
cy.get('[data-cy="sidebar-header"').trigger('mouseenter')
cy.findByTestId('sidebar-header').trigger('mouseenter')
cy.contains('.v-popper--some-open--tooltip', 'todos')
cy.percySnapshot()
cy.get('[data-cy="sidebar-header"]').trigger('mouseout')
cy.findByTestId('sidebar-header').trigger('mouseout')
cy.get('[data-e2e-href="/runs"]').trigger('mouseenter')
cy.get('[href="#/runs"]').trigger('mouseenter')
cy.contains('.v-popper--some-open--tooltip', 'Runs')
cy.get('[data-e2e-href="/runs"]').trigger('mouseout')
cy.get('[href="#/runs"]').trigger('mouseout')
cy.get('[data-e2e-href="/specs"]').trigger('mouseenter')
cy.get('[href="#/specs"]').trigger('mouseenter')
cy.contains('.v-popper--some-open--tooltip', 'Specs')
cy.get('[data-e2e-href="/specs"]').trigger('mouseout')
cy.get('[href="#/specs"]').trigger('mouseout')
cy.get('[data-e2e-href="/settings"]').trigger('mouseenter')
cy.get('[href="#/settings"]').trigger('mouseenter')
cy.contains('.v-popper--some-open--tooltip', 'Settings')
cy.get('[data-e2e-href="/settings"]').trigger('mouseout')
cy.get('[href="#/settings"]').trigger('mouseout')
})
it('opens the left nav bar when clicking the expand button (if unexpanded)', () => {
cy.findByLabelText('toggle navigation', {
selector: 'button',
}).click()
cy.findAllByText('todos').eq(1).should('not.be.visible')
cy.findByLabelText('toggle navigation', {
selector: 'button',
}).click()
cy.findByLabelText('Sidebar').closest('[aria-expanded]').should('have.attr', 'aria-expanded', 'true')
cy.findAllByText('todos').eq(1).should('be.visible')
cy.findByTestId('toggle-sidebar').click()
cy.findByTestId('sidebar').contains('todos').should('not.be.visible')
cy.findByTestId('toggle-sidebar').click()
cy.findByTestId('sidebar').contains('todos').should('be.visible')
})
it('displays the project name and opens a modal to switch testing type', () => {
cy.visitApp()
cy.findByLabelText('Sidebar').closest('[aria-expanded]').should('have.attr', 'aria-expanded', 'true')
cy.get('[data-cy="sidebar-header"]').within(() => {
cy.get('[data-cy="testing-type-e2e"]').should('be.visible')
cy.findByText('todos').should('be.visible')
cy.findByTestId('sidebar-header').within(() => {
cy.findByTestId('testing-type-e2e').should('be.visible')
cy.contains('todos').should('be.visible')
}).as('switchTestingType').click()
cy.findByRole('dialog', {
@@ -192,20 +201,13 @@ describe('Sidebar Navigation', () => {
cy.get('[aria-label="Close"]').click()
cy.findByText('Choose a testing type').should('not.exist')
cy.findByLabelText('toggle navigation', {
selector: 'button',
}).click()
cy.get('[data-cy="sidebar-header"]').click()
cy.findByTestId('toggle-sidebar').click()
cy.findByTestId('sidebar-header').click()
cy.findByText('Choose a testing type').should('be.visible')
})
it('has unlabeled menu item that shows the keyboard shortcuts modal (expanded state)', () => {
cy.findByLabelText('Sidebar').closest('[aria-expanded]').should('have.attr', 'aria-expanded', 'true')
cy.get('[data-cy="keyboard-shortcuts"]').should('be.visible')
cy.get('[data-cy="keyboard-shortcuts"]').click()
it('has menu item that shows the keyboard shortcuts modal (expanded state)', () => {
cy.findByTestId('keyboard-modal-trigger').should('be.visible').click()
cy.contains('h2', 'Keyboard Shortcuts').should('be.visible')
cy.get('li p').contains('Re-run tests').should('be.visible')
cy.get('li p').contains('Stop tests').should('be.visible')
@@ -218,20 +220,15 @@ describe('Sidebar Navigation', () => {
})
it('has a menu item labeled "Runs" which takes you to the Runs page', () => {
cy.findByLabelText('Sidebar').closest('[aria-expanded]').should('have.attr', 'aria-expanded', 'true')
cy.get('[data-cy="app-header-bar"]').findByText('Runs').should('not.exist')
cy.findByText('Runs').should('be.visible')
cy.findByText('Runs').click()
cy.findByText('Runs').should('be.visible').click()
cy.get('[data-cy="app-header-bar"]').findByText('Runs').should('be.visible')
cy.get('.router-link-active').findByText('Runs').should('be.visible')
})
it('has a menu item labeled "Specs" which takes you to the Spec List page', () => {
cy.findByLabelText('Sidebar').closest('[aria-expanded]').should('have.attr', 'aria-expanded', 'true')
cy.findByLabelText('Sidebar').within(() => {
cy.findByText('Specs').should('be.visible')
cy.findByText('Specs').click()
cy.findByTestId('sidebar').within(() => {
cy.findByText('Specs').should('be.visible').click()
})
cy.get('[data-cy="app-header-bar"]').findByText('Specs').should('be.visible')
@@ -239,18 +236,15 @@ describe('Sidebar Navigation', () => {
})
it('has a menu item labeled "Settings" which takes you to the Settings page', () => {
cy.findByLabelText('Sidebar').closest('[aria-expanded]').should('have.attr', 'aria-expanded', 'true')
cy.get('[data-cy="app-header-bar"]').findByText('Settings').should('not.exist')
cy.findByTestId('app-header-bar').findByText('Settings').should('not.exist')
cy.findByText('Settings').should('be.visible')
cy.findByText('Settings').click()
cy.get('[data-cy="app-header-bar"]').findByText('Settings').should('be.visible')
cy.findByTestId('app-header-bar').findByText('Settings').should('be.visible')
cy.get('.router-link-active').findByText('Settings').should('be.visible')
})
it('resize nav sends the correct value on the mutation', () => {
cy.contains('fixture.js').click()
cy.withCtx((ctx, o) => {
o.sinon.stub(ctx.actions.localSettings, 'setPreferences').resolves()
})

View File

@@ -26,7 +26,7 @@ describe('App: Spec List (E2E)', () => {
})
it('shows the "Specs" navigation as highlighted in the lefthand nav bar', () => {
cy.findByLabelText('Sidebar').within(() => {
cy.findByTestId('sidebar').within(() => {
cy.findByText('Specs').should('be.visible')
cy.findByText('Specs').click()
})

View File

@@ -26,7 +26,7 @@ describe('specChange subscription', () => {
describe('on config page', () => {
it('responds to configChange event when viewport is changed', () => {
cy.get('a').contains('Settings').click()
cy.contains('a', 'Settings').click()
cy.get('[data-cy="collapsible-header"]').contains('Project Settings').click()
cy.contains(`projectId: 'abc123'`)
updateProjectIdInCypressConfig('foo456')

View File

@@ -7,7 +7,7 @@ describe('KeyboardBindingsModal', () => {
return <KeyboardBindingsModal show />
})
const expectedContent = defaultMessages.sideBar.keyboardShortcuts
const expectedContent = defaultMessages.sidebar.keyboardShortcuts
Object.values(expectedContent).forEach((text) => {
cy.contains(text).should('be.visible')

View File

@@ -2,9 +2,9 @@
<StandardModal
class="transition transition-all duration-200"
variant="bare"
:title="t('sideBar.keyboardShortcuts.title')"
:title="t('sidebar.keyboardShortcuts.title')"
:model-value="show"
data-cy="switch-modal"
data-cy="keyboard-modal"
help-link=""
@update:model-value="emits('close')"
>
@@ -46,15 +46,15 @@ const emits = defineEmits<{
const keyBindings = [
{
key: ['r'],
description: t('sideBar.keyboardShortcuts.rerun'),
description: t('sidebar.keyboardShortcuts.rerun'),
},
{
key: ['s'],
description: t('sideBar.keyboardShortcuts.stop'),
description: t('sidebar.keyboardShortcuts.stop'),
},
{
key: ['f'],
description: t('sideBar.keyboardShortcuts.toggle'),
description: t('sidebar.keyboardShortcuts.toggle'),
},
]
</script>

View File

@@ -1,4 +1,5 @@
import SidebarNavigation from './SidebarNavigation.vue'
import { defaultMessages } from '@cy/i18n'
function mountComponent (initialNavExpandedVal = true) {
cy.mount(() => {
@@ -16,32 +17,42 @@ describe('SidebarNavigation', () => {
it('expands the bar when clicking the expand button', () => {
mountComponent()
cy.get('[aria-expanded]').should('have.attr', 'aria-expanded', 'false')
cy.findByText('test-project').should('not.be.visible')
cy.findByLabelText('toggle navigation', {
cy.findByLabelText(defaultMessages.sidebar.toggleLabel.collapsed, {
selector: 'button',
}).click()
cy.get('[aria-expanded]').should('have.attr', 'aria-expanded', 'true')
cy.findByLabelText(defaultMessages.sidebar.toggleLabel.expanded, {
selector: 'button',
})
cy.findByText('test-project').should('be.visible')
cy.percySnapshot()
})
it('shows tooltips on hover', () => {
mountComponent(false)
cy.get('[data-cy="sidebar-header"').trigger('mouseenter')
cy.findByTestId('sidebar-header').trigger('mouseenter')
cy.contains('.v-popper--some-open--tooltip', 'test-project').should('be.visible')
cy.get('[data-cy="sidebar-header"]').trigger('mouseout')
cy.findByTestId('sidebar-header').trigger('mouseout')
cy.get('[data-e2e-href="/runs"]').trigger('mouseenter')
cy.get('[href="#/runs"]').trigger('mouseenter')
cy.contains('.v-popper--some-open--tooltip', 'Runs').should('be.visible')
cy.get('[data-e2e-href="/runs"]').trigger('mouseout')
cy.get('[href="#/runs"]').trigger('mouseout')
cy.percySnapshot()
})
it('opens a modal to switch testing type', { viewportWidth: 1280 }, () => {
mountComponent()
cy.get('[data-cy="sidebar-header"]').click()
cy.findByTestId('sidebar-header').click()
cy.percySnapshot()
})
it('opens a modal to show keyboard shortcuts', () => {
mountComponent()
cy.findByTestId('keyboard-modal').should('not.exist')
cy.findByTestId('keyboard-modal-trigger').focus().type('{enter}')
cy.findByTestId('keyboard-modal').should('be.visible')
})
})

View File

@@ -1,19 +1,22 @@
<template>
<HideDuringScreenshot
id="sidebar"
data-cy="sidebar"
:aria-expanded="isNavBarExpanded"
class="flex flex-col bg-gray-1000 transition-all duration-300 relative"
:class="isNavBarExpanded ? 'w-248px' : 'w-64px'"
>
<button
v-if="navIsAlwaysCollapsed"
class="cursor-pointer left-full top-0 bottom-0 w-16px z-1 absolute group hocus:outline-transparent"
role="button"
aria-label="toggle navigation"
type="button"
:aria-label="isNavBarExpanded ? t('sidebar.toggleLabel.expanded') : t('sidebar.toggleLabel.collapsed')"
data-cy="toggle-sidebar"
:aria-expanded="isNavBarExpanded"
aria-controls="sidebar"
@click="toggleNavbarIfAllowed"
>
<div
data-testid="sidebar-nav-indicator"
data-cy="sidebar-nav-indicator"
class="flex h-full transform origin-left transition-transform w-16px scale-x-0 duration-300 items-center group-hocus:scale-x-100"
>
<div class="h-full bg-indigo-400 w-3px" />
@@ -31,7 +34,7 @@
/>
<nav
class="space-y-1 bg-gray-1000 flex-1"
aria-label="Sidebar"
:aria-label="t('sidebar.nav.ariaLabel')"
>
<RouterLink
v-for="item in navigation"
@@ -40,7 +43,6 @@
:to="item.href"
>
<SidebarNavigationRow
:data-e2e-href="item.href"
:active="isActive"
:icon="item.icon"
:name="item.name"
@@ -54,22 +56,22 @@
:distance="8"
:skidding="-16"
>
<div
<button
data-cy="keyboard-modal-trigger"
type="button"
class="border border-transparent rounded
cursor-pointer h-32px m-16px
p-7px transform transition-all
right-0 bottom-0 w-32px duration-300
inline-block absolute hover:border-gray-500"
:class="{ '-translate-y-48px': !isNavBarExpanded }"
:aria-label="t('sidebar.keyboardShortcuts.title')"
@click="bindingsOpen = true"
>
<i-cy-command-key_x16
class="h-16px w-16px icon-dark-gray-500"
data-cy="keyboard-shortcuts"
/>
</div>
<i-cy-command-key_x16 class="h-16px w-16px icon-dark-gray-500" />
</button>
<template #popper>
{{ t('sideBar.keyboardShortcuts.title') }}
{{ t('sidebar.keyboardShortcuts.title') }}
</template>
</Tooltip>
<KeyboardBindingsModal
@@ -79,6 +81,7 @@
<img
:src="CypressLogo"
class="h-32px m-16px w-32px"
alt="Cypress"
>
</div>
</HideDuringScreenshot>

View File

@@ -6,6 +6,8 @@
:distance="8"
>
<div
role="button"
:aria-label="`${props.gql.currentProject?.title ?? 'Cypress'} - ${t('testingType.modalTitle')}`"
class="border-b cursor-pointer flex border-gray-900 flex-shrink-0 h-64px pl-20px transition-all duration-300 items-center hover:bg-gray-900"
:popper-top-offset="4"
popper-class="h-56px"

View File

@@ -22,8 +22,7 @@
:data-selected="active"
>
<component
:is="
icon"
:is="icon"
:class="active ? 'icon-dark-indigo-300 icon-light-indigo-700' : 'icon-dark-gray-500 icon-light-gray-900 group-hover:(icon-dark-gray-300 icon-light-gray-800) group-focus:(icon-dark-gray-300 icon-light-gray-800)'"
class="flex-shrink-0 h-24px m-12px w-24px
children:transition children:duration-300"

View File

@@ -18,6 +18,7 @@
:disabled="isDisabled"
class="rounded-none border-gray-100 border-r-1px mr-12px"
variant="text"
:aria-label="t('runner.selectorPlayground.toggle')"
@click="togglePlayground"
>
<i-cy-crosshairs_x16 class="icon-dark-gray-600" />
@@ -35,6 +36,7 @@
:disabled="isDisabled"
class="border-gray-100 mr-12px"
variant="outline"
:aria-label="t('runner.selectorPlayground.toggle')"
@click="togglePlayground"
>
<i-cy-crosshairs_x16 class="icon-dark-gray-600" />
@@ -49,6 +51,7 @@
<img
class="min-w-16px w-16px"
:src="allBrowsersIcons[selectedBrowser.displayName]"
:alt="selectedBrowser.displayName"
>
{{ selectedBrowser.displayName }} {{ selectedBrowser.majorVersion }}
</template>

View File

@@ -25,7 +25,8 @@ export class AutIframe {
create (): JQuery<HTMLIFrameElement> {
const $iframe = this.$('<iframe>', {
id: `Your App: '${this.projectName}'`,
id: `Your project: '${this.projectName}'`,
title: `Your project: '${this.projectName}'`,
class: 'aut-iframe',
})

View File

@@ -9,8 +9,7 @@
"*.d.ts",
"../frontend-shared/src/**/*.vue",
"../frontend-shared/src/**/*.tsx",
"../frontend-shared/cypress/**/*.ts",
"*.d.ts"
"../frontend-shared/cypress/**/*.ts"
],
"compilerOptions": {
"noImplicitThis": true,

View File

@@ -131,13 +131,7 @@ describe('cy.session', { retries: 0 }, () => {
message: '(new) session-1',
})
expect(logs[1].get()).to.contain({
name: 'session',
message: 'session-1',
group: sessionGroupId,
})
const createNewSessionGroup = logs[2].get()
const createNewSessionGroup = logs[1].get()
expect(createNewSessionGroup).to.contain({
displayName: 'Create New Session',
@@ -145,29 +139,29 @@ describe('cy.session', { retries: 0 }, () => {
group: sessionGroupId,
})
expect(logs[3].get()).to.contain({
expect(logs[2].get()).to.contain({
name: 'Clear Page',
group: createNewSessionGroup.id,
})
expect(logs[4].get()).to.deep.contain({
expect(logs[3].get()).to.deep.contain({
alias: ['setupSession'],
group: createNewSessionGroup.id,
})
expect(logs[5].get()).to.contain({
expect(logs[4].get()).to.contain({
name: 'Clear Page',
group: createNewSessionGroup.id,
})
expect(logs[6].get()).to.contain({
expect(logs[5].get()).to.contain({
name: 'Clear Page',
group: sessionGroupId,
})
})
it('creates new session instrument with session details', () => {
const sessionInfo = logs[1].get('sessionInfo')
const sessionInfo = logs[0].get('sessionInfo')
expect(sessionInfo).to.deep.eq({
id: 'session-1',
@@ -176,10 +170,11 @@ describe('cy.session', { retries: 0 }, () => {
})
it('has session details in the consoleProps', () => {
const consoleProps = logs[1].get('consoleProps')()
const consoleProps = logs[0].get('consoleProps')()
expect(consoleProps).to.deep.eq({
Command: 'session',
Snapshot: 'The snapshot is missing. Displaying current state of the DOM.',
id: 'session-1',
table: [],
})
@@ -213,13 +208,7 @@ describe('cy.session', { retries: 0 }, () => {
message: '(new) session-1',
})
expect(logs[1].get()).to.contain({
name: 'session',
message: 'session-1',
group: sessionGroupId,
})
const createNewSessionGroup = logs[2].get()
const createNewSessionGroup = logs[1].get()
expect(createNewSessionGroup).to.contain({
displayName: 'Create New Session',
@@ -227,34 +216,34 @@ describe('cy.session', { retries: 0 }, () => {
group: sessionGroupId,
})
expect(logs[3].get()).to.contain({
expect(logs[2].get()).to.contain({
name: 'Clear Page',
group: createNewSessionGroup.id,
})
expect(logs[4].get()).to.deep.contain({
expect(logs[3].get()).to.deep.contain({
alias: ['setupSession'],
group: createNewSessionGroup.id,
})
expect(logs[5].get()).to.contain({
expect(logs[4].get()).to.contain({
name: 'Clear Page',
group: createNewSessionGroup.id,
})
const validateSessionGroup = logs[6].get()
const validateSessionGroup = logs[5].get()
expect(validateSessionGroup).to.contain({
displayName: 'Validate Session: valid',
group: sessionGroupId,
})
expect(logs[7].get()).to.deep.contain({
expect(logs[6].get()).to.deep.contain({
alias: ['validateSession'],
group: validateSessionGroup.id,
})
expect(logs[8].get()).to.contain({
expect(logs[7].get()).to.contain({
name: 'Clear Page',
group: sessionGroupId,
})
@@ -282,13 +271,7 @@ describe('cy.session', { retries: 0 }, () => {
message: '(new) session-1',
})
expect(logs[1].get()).to.contain({
name: 'session',
message: 'session-1',
group: sessionGroupId,
})
const createNewSessionGroup = logs[2].get()
const createNewSessionGroup = logs[1].get()
expect(createNewSessionGroup).to.contain({
displayName: 'Create New Session',
@@ -296,22 +279,22 @@ describe('cy.session', { retries: 0 }, () => {
group: sessionGroupId,
})
expect(logs[3].get()).to.contain({
expect(logs[2].get()).to.contain({
name: 'Clear Page',
group: createNewSessionGroup.id,
})
expect(logs[4].get()).to.deep.contain({
expect(logs[3].get()).to.deep.contain({
alias: ['setupSession'],
group: createNewSessionGroup.id,
})
expect(logs[5].get()).to.contain({
expect(logs[4].get()).to.contain({
name: 'Clear Page',
group: createNewSessionGroup.id,
})
const validateSessionGroup = logs[6].get()
const validateSessionGroup = logs[5].get()
expect(validateSessionGroup).to.contain({
displayName: 'Validate Session: invalid',
@@ -359,13 +342,7 @@ describe('cy.session', { retries: 0 }, () => {
message: '(saved) session-1',
})
expect(logs[1].get()).to.contain({
name: 'session',
message: 'session-1',
group: sessionGroupId,
})
const restoreSavedSessionGroup = logs[2].get()
const restoreSavedSessionGroup = logs[1].get()
expect(restoreSavedSessionGroup).to.contain({
displayName: 'Restore Saved Session',
@@ -373,12 +350,12 @@ describe('cy.session', { retries: 0 }, () => {
group: sessionGroupId,
})
expect(logs[3].get()).to.contain({
expect(logs[2].get()).to.contain({
name: 'Clear Page',
group: restoreSavedSessionGroup.id,
})
expect(logs[4].get()).to.contain({
expect(logs[3].get()).to.contain({
name: 'Clear Page',
group: sessionGroupId,
})
@@ -417,13 +394,7 @@ describe('cy.session', { retries: 0 }, () => {
message: '(saved) session-1',
})
expect(logs[1].get()).to.contain({
name: 'session',
message: 'session-1',
group: sessionGroupId,
})
const restoreSavedSessionGroup = logs[2].get()
const restoreSavedSessionGroup = logs[1].get()
expect(restoreSavedSessionGroup).to.contain({
displayName: 'Restore Saved Session',
@@ -431,24 +402,24 @@ describe('cy.session', { retries: 0 }, () => {
group: sessionGroupId,
})
expect(logs[3].get()).to.contain({
expect(logs[2].get()).to.contain({
name: 'Clear Page',
group: restoreSavedSessionGroup.id,
})
const validateSessionGroup = logs[4].get()
const validateSessionGroup = logs[3].get()
expect(validateSessionGroup).to.contain({
displayName: 'Validate Session: valid',
group: sessionGroupId,
})
expect(logs[5].get()).to.deep.contain({
expect(logs[4].get()).to.deep.contain({
alias: ['validateSession'],
group: validateSessionGroup.id,
})
expect(logs[6].get()).to.contain({
expect(logs[5].get()).to.contain({
name: 'Clear Page',
group: sessionGroupId,
})
@@ -492,13 +463,7 @@ describe('cy.session', { retries: 0 }, () => {
message: '(recreated) session-1',
})
expect(logs[1].get()).to.contain({
name: 'session',
message: 'session-1',
group: sessionGroupId,
})
const recreatedSavedSessionGroup = logs[2].get()
const recreatedSavedSessionGroup = logs[1].get()
expect(recreatedSavedSessionGroup).to.contain({
displayName: 'Restore Saved Session',
@@ -506,31 +471,31 @@ describe('cy.session', { retries: 0 }, () => {
group: sessionGroupId,
})
expect(logs[3].get()).to.contain({
expect(logs[2].get()).to.contain({
name: 'Clear Page',
group: recreatedSavedSessionGroup.id,
})
const validateSessionGroup = logs[4].get()
const validateSessionGroup = logs[3].get()
expect(validateSessionGroup).to.contain({
displayName: 'Validate Session: invalid',
group: sessionGroupId,
})
expect(logs[5].get()).to.deep.contain({
expect(logs[4].get()).to.deep.contain({
alias: ['validateSession'],
group: validateSessionGroup.id,
})
expect(logs[6].get()).to.deep.contain({
expect(logs[5].get()).to.deep.contain({
showError: true,
group: validateSessionGroup.id,
})
expect(logs[6].get('error').message).to.eq('Your `cy.session` **validate** callback returned false.')
expect(logs[5].get('error').message).to.eq('Your `cy.session` **validate** callback returned false.')
const createNewSessionGroup = logs[7].get()
const createNewSessionGroup = logs[6].get()
expect(createNewSessionGroup).to.contain({
displayName: 'Create New Session',
@@ -538,34 +503,34 @@ describe('cy.session', { retries: 0 }, () => {
group: sessionGroupId,
})
expect(logs[8].get()).to.contain({
expect(logs[7].get()).to.contain({
name: 'Clear Page',
group: createNewSessionGroup.id,
})
expect(logs[9].get()).to.deep.contain({
expect(logs[8].get()).to.deep.contain({
alias: ['setupSession'],
group: createNewSessionGroup.id,
})
expect(logs[10].get()).to.contain({
expect(logs[9].get()).to.contain({
name: 'Clear Page',
group: createNewSessionGroup.id,
})
const secondValidateSessionGroup = logs[11].get()
const secondValidateSessionGroup = logs[10].get()
expect(secondValidateSessionGroup).to.contain({
displayName: 'Validate Session: valid',
group: sessionGroupId,
})
expect(logs[12].get()).to.deep.contain({
expect(logs[11].get()).to.deep.contain({
alias: ['validateSession'],
group: secondValidateSessionGroup.id,
})
expect(logs[13].get()).to.contain({
expect(logs[12].get()).to.contain({
name: 'Clear Page',
group: sessionGroupId,
})
@@ -599,13 +564,7 @@ describe('cy.session', { retries: 0 }, () => {
message: '(recreated) session-1',
})
expect(logs[1].get()).to.contain({
name: 'session',
message: 'session-1',
group: sessionGroupId,
})
const recreatedSavedSessionGroup = logs[2].get()
const recreatedSavedSessionGroup = logs[1].get()
expect(recreatedSavedSessionGroup).to.contain({
displayName: 'Restore Saved Session',
@@ -613,31 +572,31 @@ describe('cy.session', { retries: 0 }, () => {
group: sessionGroupId,
})
expect(logs[3].get()).to.contain({
expect(logs[2].get()).to.contain({
name: 'Clear Page',
group: recreatedSavedSessionGroup.id,
})
const validateSessionGroup = logs[4].get()
const validateSessionGroup = logs[3].get()
expect(validateSessionGroup).to.contain({
displayName: 'Validate Session: invalid',
group: sessionGroupId,
})
expect(logs[5].get()).to.deep.contain({
expect(logs[4].get()).to.deep.contain({
alias: ['validateSession'],
group: validateSessionGroup.id,
})
expect(logs[6].get()).to.deep.contain({
expect(logs[5].get()).to.deep.contain({
showError: true,
group: validateSessionGroup.id,
})
expect(logs[6].get('error').message).to.eq('Your `cy.session` **validate** callback returned false.')
expect(logs[5].get('error').message).to.eq('Your `cy.session` **validate** callback returned false.')
const createNewSessionGroup = logs[7].get()
const createNewSessionGroup = logs[6].get()
expect(createNewSessionGroup).to.contain({
displayName: 'Create New Session',
@@ -645,29 +604,29 @@ describe('cy.session', { retries: 0 }, () => {
group: sessionGroupId,
})
expect(logs[8].get()).to.contain({
expect(logs[7].get()).to.contain({
name: 'Clear Page',
group: createNewSessionGroup.id,
})
expect(logs[9].get()).to.deep.contain({
expect(logs[8].get()).to.deep.contain({
alias: ['setupSession'],
group: createNewSessionGroup.id,
})
expect(logs[10].get()).to.contain({
expect(logs[9].get()).to.contain({
name: 'Clear Page',
group: createNewSessionGroup.id,
})
const secondValidateSessionGroup = logs[11].get()
const secondValidateSessionGroup = logs[10].get()
expect(secondValidateSessionGroup).to.contain({
displayName: 'Validate Session: invalid',
group: sessionGroupId,
})
expect(logs[12].get()).to.deep.contain({
expect(logs[11].get()).to.deep.contain({
alias: ['validateSession'],
group: secondValidateSessionGroup.id,
})

View File

@@ -121,13 +121,8 @@ export default function (Commands, Cypress, cy) {
name: 'session',
message: `${existingSession.id.length > 50 ? `${existingSession.id.substr(0, 47)}...` : existingSession.id}`,
groupStart: true,
snapshot: false,
})
const dataLog = Cypress.log({
name: 'session',
sessionInfo: getSessionDetails(existingSession),
message: `${existingSession.id.length > 50 ? `${existingSession.id.substr(0, 47)}...` : existingSession.id}`,
snapshot: false,
})
function createSession (existingSession, recreateSession = false) {
@@ -172,9 +167,7 @@ export default function (Commands, Cypress, cy) {
sessionsManager.setActiveSession({ [existingSession.id]: existingSession })
dataLog.set({
consoleProps: () => getConsoleProps(existingSession),
})
_log.set({ consoleProps: () => getConsoleProps(existingSession) })
// persist the session to the server. Only matters in openMode OR if there's a top navigation on a future test.
// eslint-disable-next-line no-console
@@ -197,6 +190,7 @@ export default function (Commands, Cypress, cy) {
await navigateAboutBlank()
_log.set({
consoleProps: () => getConsoleProps(existingSession),
renderProps: () => {
return {
indicator: 'pending',
@@ -205,10 +199,6 @@ export default function (Commands, Cypress, cy) {
},
})
dataLog.set({
consoleProps: () => getConsoleProps(existingSession),
})
await sessions.setSessionData(existingSession)
Cypress.log({ groupEnd: true, emitOnly: true })
})

View File

@@ -14,6 +14,8 @@ import type { E2ETaskMap } from '../e2ePluginSetup'
import type { SinonStub } from 'sinon'
import type sinon from 'sinon'
import type pDefer from 'p-defer'
import 'cypress-plugin-tab'
import 'cypress-axe'
import type { Response } from 'cross-fetch'
configure({ testIdAttribute: 'data-cy' })
@@ -497,7 +499,6 @@ function validateExternalLink (subject, options: ValidateExternalLinkOptions | s
function tabUntil (fn: (el: JQuery<HTMLElement>) => boolean, limit: number = 10) {
function _tabUntil (step: number) {
// @ts-expect-error
return cy.tab().focused({ log: false }).then((el) => {
const pass = fn(el)

View File

@@ -38,8 +38,11 @@
"@vue/compiler-sfc": "3.2.31",
"@vueuse/core": "7.2.2",
"@windicss/plugin-interaction-variants": "1.0.0",
"axe-core": "4.4.1",
"combine-properties": "0.1.0",
"cross-env": "6.0.3",
"cypress-axe": "0.14.0",
"cypress-plugin-tab": "1.0.5",
"cypress-real-events": "1.6.0",
"dayjs": "^1.9.3",
"fake-uuid": "^1.0.0",

View File

@@ -108,6 +108,7 @@
<img
class="w-16px filter group-hocus:grayscale-0"
data-cy="top-nav-active-browser-icon"
:alt="props.gql?.currentProject?.activeBrowser?.displayName"
:class="open ? 'grayscale-0' : 'grayscale'"
:src="allBrowsersIcons[props.gql?.currentProject?.activeBrowser?.displayName] || allBrowsersIcons.generic"
>

View File

@@ -134,12 +134,19 @@
"defaultMessage": "No results matched your search:",
"clearSearch": "Clear Search"
},
"sideBar": {
"sidebar": {
"keyboardShortcuts": {
"title": "Keyboard Shortcuts",
"rerun": "Re-run tests",
"stop": "Stop tests",
"toggle": "Toggle specs list"
},
"toggleLabel": {
"expanded": "Collapse sidebar",
"collapsed": "Expand sidebar"
},
"nav": {
"ariaLabel": "Pages"
}
},
"topNav": {
@@ -662,7 +669,8 @@
"printTooltip": "Print to console",
"printTooltipAction": "Printed!",
"invalidSelector": "Invalid",
"selectorMethodsLabel": "Selector Methods"
"selectorMethodsLabel": "Selector Methods",
"toggle": "Toggle playground"
},
"automation": {
"disconnected": {

View File

@@ -61,7 +61,8 @@
"@intlify/vite-plugin-vue-i18n/client",
"@testing-library/cypress",
"cypress-real-events",
"cypress"
"cypress",
"cypress-axe"
],
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,

View File

@@ -46,7 +46,6 @@
"classnames": "2.3.1",
"concurrently": "^6.2.0",
"cross-env": "6.0.3",
"cypress-plugin-tab": "1.0.5",
"cypress-real-events": "1.6.0",
"dedent": "^0.7.0",
"disparity": "^3.0.0",

View File

@@ -29,3 +29,9 @@ yarn workspace @packages/proxy test
```
Additionally, the `server` package contains tests that use the `proxy`.
## Debug Logs
High level logs are available in the `DEBUG=cypress:proxy:*` namespace.
Detailed per-request logs are available in `DEBUG=cypress-verbose:proxy:http`.

View File

@@ -1,11 +1,14 @@
import debugModule from 'debug'
import * as errors from '@packages/server/lib/errors'
import type { HttpMiddleware } from '.'
import type { Readable } from 'stream'
import { InterceptError } from '@packages/net-stubbing'
import type { Request } from '@cypress/request'
const debug = debugModule('cypress:proxy:http:error-middleware')
// do not use a debug namespace in this file - use the per-request `this.debug` instead
// available as cypress-verbose:proxy:http
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const debug = null
export type ErrorMiddleware = HttpMiddleware<{
error: Error
@@ -14,7 +17,7 @@ export type ErrorMiddleware = HttpMiddleware<{
}>
const LogError: ErrorMiddleware = function () {
debug('error proxying request %o', {
this.debug('error proxying request %o', {
error: this.error,
url: this.req.url,
headers: this.req.headers,
@@ -36,7 +39,7 @@ const SendToDriver: ErrorMiddleware = function () {
export const AbortRequest: ErrorMiddleware = function () {
if (this.outgoingReq) {
debug('aborting outgoingReq')
this.debug('aborting outgoingReq')
this.outgoingReq.abort()
}
@@ -45,7 +48,7 @@ export const AbortRequest: ErrorMiddleware = function () {
export const UnpipeResponse: ErrorMiddleware = function () {
if (this.incomingResStream) {
debug('unpiping resStream from response')
this.debug('unpiping resStream from response')
this.incomingResStream.unpipe()
}

View File

@@ -7,6 +7,7 @@ import type {
BrowserPreRequest,
} from '@packages/proxy'
import Debug from 'debug'
import chalk from 'chalk'
import ErrorMiddleware from './error-middleware'
import { HttpBuffers } from './util/buffers'
import { GetPreRequestCb, PreRequests } from './util/prerequests'
@@ -21,6 +22,12 @@ import { DeferredSourceMapCache } from '@packages/rewriter'
import type { Browser } from '@packages/server/lib/browsers/types'
import type { RemoteStates } from '@packages/server/lib/remote_states'
function getRandomColorFn () {
return chalk.hex(`#${Number(
Math.floor(Math.random() * 0xFFFFFF),
).toString(16).padStart(6, 'F').toUpperCase()}`)
}
export const debugVerbose = Debug('cypress-verbose:proxy:http')
export enum HttpStages {
@@ -231,7 +238,12 @@ export class Http {
}
}
handle (req: Request, res: Response) {
handle (req: CypressIncomingRequest, res: CypressOutgoingResponse) {
const colorFn = debugVerbose.enabled ? getRandomColorFn() : undefined
const debugUrl = debugVerbose.enabled ?
(req.proxiedUrl.length > 80 ? `${req.proxiedUrl.slice(0, 80)}...` : req.proxiedUrl)
: undefined
const ctx: HttpMiddlewareCtx<any> = {
req,
res,
@@ -247,7 +259,9 @@ export class Http {
socket: this.socket,
serverBus: this.serverBus,
debug: (formatter, ...args) => {
debugVerbose(`%s %s %s ${formatter}`, ctx.req.method, ctx.req.proxiedUrl, ctx.stage, ...args)
if (!debugVerbose.enabled) return
debugVerbose(`${colorFn!(`%s %s`)} %s ${formatter}`, req.method, debugUrl, chalk.grey(ctx.stage), ...args)
},
deferSourceMapRewrite: (opts) => {
this.deferredSourceMapCache.defer({

View File

@@ -1,17 +1,19 @@
import _ from 'lodash'
import { blocked, cors } from '@packages/network'
import { InterceptRequest } from '@packages/net-stubbing'
import debugModule from 'debug'
import type { HttpMiddleware } from './'
// do not use a debug namespace in this file - use the per-request `this.debug` instead
// available as cypress-verbose:proxy:http
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const debug = null
export type RequestMiddleware = HttpMiddleware<{
outgoingReq: any
}>
const debug = debugModule('cypress:proxy:http:request-middleware')
const LogRequest: RequestMiddleware = function () {
debug('proxying request %o', {
this.debug('proxying request %o', {
req: _.pick(this.req, 'method', 'proxiedUrl', 'headers'),
})

View File

@@ -1,9 +1,9 @@
import _ from 'lodash'
import charset from 'charset'
import type Debug from 'debug'
import type { CookieOptions } from 'express'
import { cors, concatStream, httpUtils, uri } from '@packages/network'
import type { CypressIncomingRequest, CypressOutgoingResponse } from '@packages/proxy'
import debugModule from 'debug'
import type { HttpMiddleware, HttpMiddlewareThis } from '.'
import iconv from 'iconv-lite'
import type { IncomingMessage, IncomingHttpHeaders } from 'http'
@@ -29,7 +29,10 @@ interface ResponseMiddlewareProps {
export type ResponseMiddleware = HttpMiddleware<ResponseMiddlewareProps>
const debug = debugModule('cypress:proxy:http:response-middleware')
// do not use a debug namespace in this file - use the per-request `this.debug` instead
// available as cypress-verbose:proxy:http
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const debug = null
// https://github.com/cypress-io/cypress/issues/1756
const zlibOptions = {
@@ -38,7 +41,7 @@ const zlibOptions = {
}
// https://github.com/cypress-io/cypress/issues/1543
function getNodeCharsetFromResponse (headers: IncomingHttpHeaders, body: Buffer) {
function getNodeCharsetFromResponse (headers: IncomingHttpHeaders, body: Buffer, debug: Debug.Debugger) {
const httpCharset = (charset(headers, body, 1024) || '').toLowerCase()
debug('inferred charset from response %o', { httpCharset })
@@ -133,7 +136,7 @@ const stringifyFeaturePolicy = (policy: any): string => {
}
const LogResponse: ResponseMiddleware = function () {
debug('received response %o', {
this.debug('received response %o', {
req: _.pick(this.req, 'method', 'proxiedUrl', 'headers'),
incomingRes: _.pick(this.incomingRes, 'headers', 'statusCode'),
})
@@ -143,10 +146,10 @@ const LogResponse: ResponseMiddleware = function () {
const AttachPlainTextStreamFn: ResponseMiddleware = function () {
this.makeResStreamPlainText = function () {
debug('ensuring resStream is plaintext')
this.debug('ensuring resStream is plaintext')
if (!this.isGunzipped && resIsGzipped(this.incomingRes)) {
debug('gunzipping response body')
this.debug('gunzipping response body')
const gunzip = zlib.createGunzip(zlibOptions)
@@ -193,6 +196,8 @@ const PatchExpressSetHeader: ResponseMiddleware = function () {
let kOutHeaders
const ctxDebug = this.debug
this.res.setHeader = function (name, value) {
// express.Response.setHeader does all kinds of silly/nasty stuff to the content-type...
// but we don't want to change it at all!
@@ -209,7 +214,7 @@ const PatchExpressSetHeader: ResponseMiddleware = function () {
throw err
}
debug('setHeader error ignored %o', { name, value, code: err.code, err })
ctxDebug('setHeader error ignored %o', { name, value, code: err.code, err })
if (!kOutHeaders) {
kOutHeaders = getKOutHeadersSymbol()
@@ -334,7 +339,7 @@ const SetInjectionLevel: ResponseMiddleware = function () {
|| resContentTypeIsJavaScript(this.incomingRes)
)
debug('injection levels: %o', _.pick(this.res, 'isInitial', 'wantsInjection', 'wantsSecurityRemoved'))
this.debug('injection levels: %o', _.pick(this.res, 'isInitial', 'wantsInjection', 'wantsSecurityRemoved'))
this.next()
}
@@ -410,20 +415,21 @@ interface EnsureSameSiteNoneProps {
browser: Browser | { family: string | null }
isLocalhost: boolean
url: URL
ctxDebug: Debug.Debugger
}
const cookieSameSiteRegex = /SameSite=(\w+)/i
const cookieSecureRegex = /(^|\W)Secure(\W|$)/i
const cookieSecureSemicolonRegex = /;\s*Secure/i
const ensureSameSiteNone = ({ cookie, browser, isLocalhost, url }: EnsureSameSiteNoneProps) => {
debug('original cookie: %s', cookie)
const ensureSameSiteNone = ({ cookie, browser, isLocalhost, url, ctxDebug }: EnsureSameSiteNoneProps) => {
ctxDebug('original cookie: %s', cookie)
if (cookieSameSiteRegex.test(cookie)) {
debug('change cookie to SameSite=None')
ctxDebug('change cookie to SameSite=None')
cookie = cookie.replace(cookieSameSiteRegex, 'SameSite=None')
} else {
debug('add SameSite=None to cookie')
ctxDebug('add SameSite=None to cookie')
cookie += '; SameSite=None'
}
@@ -439,15 +445,15 @@ const ensureSameSiteNone = ({ cookie, browser, isLocalhost, url }: EnsureSameSit
// remove Secure from http://localhost cookies in Firefox.
if (cookieSecureRegex.test(cookie)) {
if (isFirefox && isLocalhost && url.protocol === 'http:') {
debug('remove Secure from cookie')
ctxDebug('remove Secure from cookie')
cookie = cookie.replace(cookieSecureSemicolonRegex, '')
}
} else if (!isFirefox || url.protocol === 'https:') {
debug('add Secure to cookie')
ctxDebug('add Secure to cookie')
cookie += '; Secure'
}
debug('resulting cookie: %s', cookie)
ctxDebug('resulting cookie: %s', cookie)
return cookie
}
@@ -464,17 +470,17 @@ const CopyCookiesFromIncomingRes: ResponseMiddleware = function () {
const url = new URL(this.req.proxiedUrl)
const isLocalhost = uri.isLocalhost(url)
debug('force SameSite=None?', needsCrossOriginHandling)
this.debug('force SameSite=None?', needsCrossOriginHandling)
;([] as string[]).concat(cookies).forEach((cookie) => {
if (needsCrossOriginHandling) {
cookie = ensureSameSiteNone({ cookie, browser, isLocalhost, url })
cookie = ensureSameSiteNone({ cookie, browser, isLocalhost, url, ctxDebug: this.debug })
}
try {
this.res.append('Set-Cookie', cookie)
} catch (err) {
debug('failed to Set-Cookie, continuing %o', { err, cookie })
this.debug('failed to Set-Cookie, continuing %o', { err, cookie })
}
})
}
@@ -495,7 +501,7 @@ const MaybeSendRedirectToClient: ResponseMiddleware = function () {
setInitialCookie(this.res, this.remoteStates.current(), true)
debug('redirecting to new url %o', { statusCode, newUrl })
this.debug('redirecting to new url %o', { statusCode, newUrl })
this.res.redirect(Number(statusCode), newUrl)
return this.end()
@@ -528,12 +534,13 @@ const MaybeInjectHtml: ResponseMiddleware = function () {
this.skipMiddleware('MaybeRemoveSecurity') // we only want to do one or the other
debug('injecting into HTML')
this.debug('injecting into HTML')
this.makeResStreamPlainText()
this.incomingResStream.pipe(concatStream(async (body) => {
const nodeCharset = getNodeCharsetFromResponse(this.incomingRes.headers, body)
const nodeCharset = getNodeCharsetFromResponse(this.incomingRes.headers, body, this.debug)
const decodedBody = iconv.decode(body, nodeCharset)
const injectedBody = await rewriter.html(decodedBody, {
domainName: cors.getDomainNameFromUrl(this.req.proxiedUrl),
@@ -561,7 +568,7 @@ const MaybeRemoveSecurity: ResponseMiddleware = function () {
return this.next()
}
debug('removing JS framebusting code')
this.debug('removing JS framebusting code')
this.makeResStreamPlainText()
@@ -578,7 +585,7 @@ const MaybeRemoveSecurity: ResponseMiddleware = function () {
const GzipBody: ResponseMiddleware = function () {
if (this.isGunzipped) {
debug('regzipping response body')
this.debug('regzipping response body')
this.incomingResStream = this.incomingResStream.pipe(zlib.createGzip(zlibOptions)).on('error', this.onError)
}

View File

@@ -14,6 +14,7 @@
},
"dependencies": {
"bluebird": "3.5.3",
"chalk": "2.4.2",
"charset": "1.0.1",
"common-tags": "1.8.0",
"debug": "^4.3.2",

View File

@@ -44,6 +44,7 @@ describe('http', function () {
})
return new Http(httpOpts)
// @ts-ignore
.handle({}, {})
.then(function () {
expect(incomingRequest, 'incomingRequest').to.be.calledOnce
@@ -61,6 +62,7 @@ describe('http', function () {
})
return new Http(httpOpts)
// @ts-ignore
.handle({}, {})
.then(function () {
expect(incomingRequest).to.be.calledOnce
@@ -83,6 +85,7 @@ describe('http', function () {
})
return new Http(httpOpts)
// @ts-ignore
.handle({}, {})
.then(function () {
expect(incomingRequest).to.be.calledOnce
@@ -145,6 +148,7 @@ describe('http', function () {
middleware[HttpStages.Error].push(error2)
return new Http(httpOpts)
// @ts-ignore
.handle({}, {})
.then(function () {
[

View File

@@ -400,7 +400,7 @@ const _isAUTFrame = async (frameId: string) => {
}
const frame = _.find(frameTree?.childFrames || [], ({ frame }) => {
return frame?.name?.startsWith('Your App:')
return frame?.name?.startsWith('Your project:')
}) as HasFrame | undefined
if (frame) {

View File

@@ -381,7 +381,7 @@ describe('lib/browsers/chrome', () => {
{
frame: {
id: 'aut-frame-id',
name: 'Your App: "FakeBlock"',
name: 'Your project: "FakeBlock"',
},
},
{

View File

@@ -10291,6 +10291,11 @@ aws4@^1.8.0:
resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.11.0.tgz#d61f46d83b2519250e2784daf5b09479a8b41c59"
integrity sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==
axe-core@4.4.1:
version "4.4.1"
resolved "https://registry.yarnpkg.com/axe-core/-/axe-core-4.4.1.tgz#7dbdc25989298f9ad006645cd396782443757413"
integrity sha512-gd1kmb21kwNuWr6BQz8fv6GNECPBnUasepcoLbekws23NVBLODdsClRZ+bQ8+9Uomf3Sm3+Vwn0oYG9NvwnJCw==
axios@0.21.2:
version "0.21.2"
resolved "https://registry.yarnpkg.com/axios/-/axios-0.21.2.tgz#21297d5084b2aeeb422f5d38e7be4fbb82239017"
@@ -14915,6 +14920,11 @@ cyclist@^1.0.1:
resolved "https://registry.yarnpkg.com/cyclist/-/cyclist-1.0.1.tgz#596e9698fd0c80e12038c2b82d6eb1b35b6224d9"
integrity sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=
cypress-axe@0.14.0:
version "0.14.0"
resolved "https://registry.yarnpkg.com/cypress-axe/-/cypress-axe-0.14.0.tgz#5f5e70fb36b8cb3ba73a8ba01e9262ff1268d5e2"
integrity sha512-7Rdjnko0MjggCmndc1wECAkvQBIhuy+DRtjF7bd5YPZRFvubfMNvrxfqD8PWQmxm7MZE0ffS4Xr43V6ZmvLopg==
"cypress-example-kitchensink@https://github.com/cypress-io/cypress-example-kitchensink.git#b709bad8f1e29eb018e32383014cfd6f9a192691":
version "0.0.0-development"
resolved "https://github.com/cypress-io/cypress-example-kitchensink.git#b709bad8f1e29eb018e32383014cfd6f9a192691"