chore(12): merge develop into release/12.0.0 (#24927)

Co-authored-by: Lachlan Miller <lachlan.miller.1990@outlook.com>
Co-authored-by: Mark Noonan <mark@cypress.io>
Co-authored-by: Matt Henkes <mjhenkes@gmail.com>
Co-authored-by: Mike Plummer <mike-plummer@users.noreply.github.com>
Co-authored-by: Zach Bloomquist <git@chary.us>
Co-authored-by: Chris Breiding <chrisbreiding@users.noreply.github.com>
Co-authored-by: Zachary Williams <ZachJW34@gmail.com>
Co-authored-by: Feng Yu <abcfy2@users.noreply.github.com>
This commit is contained in:
Emily Rohrbough
2022-12-01 19:33:44 -06:00
committed by GitHub
parent 98efdf45b3
commit 05530ce531
58 changed files with 1394 additions and 853 deletions

View File

@@ -613,8 +613,9 @@ commands:
- install-webkit-deps
- run:
name: Run system tests
environment:
CYPRESS_COMMERCIAL_RECOMMENDATIONS: '0'
command: |
CYPRESS_COMMERCIAL_RECOMMENDATIONS=0
ALL_SPECS=`circleci tests glob "/root/cypress/system-tests/test/*spec*"`
SPECS=
for file in $ALL_SPECS; do
@@ -641,8 +642,9 @@ commands:
- restore_cached_system_tests_deps
- run:
name: Run system tests
environment:
CYPRESS_COMMERCIAL_RECOMMENDATIONS: '0'
command: |
CYPRESS_COMMERCIAL_RECOMMENDATIONS=0
ALL_SPECS=`circleci tests glob "$HOME/cypress/system-tests/test-binary/*spec*"`
SPECS=`echo $ALL_SPECS | xargs -n 1 | circleci tests split --split-by=timings`
echo SPECS=$SPECS
@@ -1325,7 +1327,7 @@ jobs:
condition:
equal: [ *darwin-arm64-executor, << parameters.executor >> ]
steps:
- run: rm -f /tmp/cypress/junit/*
- run: rm -f /tmp/cypress/junit/*
- unless:
condition:
or:
@@ -1514,7 +1516,9 @@ jobs:
steps:
- restore_cached_workspace
- run:
command: CYPRESS_COMMERCIAL_RECOMMENDATIONS=0 yarn workspace @tooling/system-tests test:ci "test/non_root*spec*" --browser electron
environment:
CYPRESS_COMMERCIAL_RECOMMENDATIONS: '0'
command: yarn workspace @tooling/system-tests test:ci "test/non_root*spec*" --browser electron
- verify-mocha-results
- store_test_results:
path: /tmp/cypress

View File

@@ -34,6 +34,12 @@ module.exports = {
'plugin:@cypress/dev/tests',
],
parser: '@typescript-eslint/parser',
ignorePatterns: [
// cli types are checked by dtslint
'cli/types/**',
// these fixtures are supposed to fail linting
'npm/eslint-plugin-dev/test/fixtures/**',
],
overrides: [
{
files: [

View File

@@ -112,9 +112,9 @@ video | Problems with video recordings | [open](https://github.com/cypress-io/cy
## Writing Documentation
Cypress documentation lives in a separate repository with its own dependencies and build tools.
See [Documentation Contributing Guideline](https://github.com/cypress-io/cypress-documentation/blob/master/CONTRIBUTING.md).
See [Documentation Contributing Guidelines](https://github.com/cypress-io/cypress-documentation/blob/master/CONTRIBUTING.md).
## Writing code
@@ -164,7 +164,7 @@ Here is a list of the core packages in this repository with a short description,
| [ts](./packages/ts) | `@packages/ts` | A centralized version of typescript. |
| [types](./packages/types) | `@packages/types` | The shared internal Cypress types. |
| [v8-snapshot-require](./packages/v8-snapshot-require) | `@packages/v8-snapshot-requie` | Tool to load a snapshot for Electron applications that was created by `@tooling/v8-snapshot`. |
| [web-config](./packages/web-config) | `@packages/ui-components` | The web-related configuration. |
| [web-config](./packages/web-config) | `@packages/web-config` | The web-related configuration. |
Private packages involved in development of the app live within the [`tooling`](./tooling) directory and are in the `@tooling/` namespace. They are discrete modules with different responsibilities, but each is necessary for development of the Cypress app and is not necessarily useful outside of the Cypress app.
@@ -358,7 +358,7 @@ This is to ensure that links do not go dead in older versions of Cypress when th
### Tests
For most packages there are typically unit and integration tests.
For most packages there are typically unit and integration tests. For UI packages there are E2E and component tests.
Please refer to each packages' `README.md` which documents how to run tests. It is not feasible to try to run all of the tests together. We run our entire test fleet across over a dozen containers in CI.
@@ -488,22 +488,28 @@ We do not continuously deploy the Cypress binary, so `develop` contains all of t
- When opening a PR for a specific issue already open, please name the branch you are working on using the convention `issue-[issue number]`. For example, if your PR fixes Issue #803, name your branch `issue-803`. If the PR is a larger issue, you can add more context like `issue-803-new-scrollable-area`. If there's not an associated open issue, **[create an issue](https://github.com/cypress-io/cypress/issues/new/choose)**.
- PRs can be opened before all the work is finished. In fact we encourage this! Please create a [Draft Pull Request](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests#draft-pull-requests) if your PR is not ready for review. [Mark the PR as **Ready for Review**](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/changing-the-stage-of-a-pull-request#marking-a-pull-request-as-ready-for-review) when you're ready for a Cypress team member to review the PR.
- Prefix the title of the Pull Request using [semantic-release](https://github.com/semantic-release/semantic-release)'s format as defined [here](https://github.com/angular/angular.js/blob/master/DEVELOPERS.md#type). For example, if your PR is fixing a bug, you should prefix the PR title with `fix:`.
- Fill out the [Pull Request Template](./.github/PULL_REQUEST_TEMPLATE.md) completely within the body of the PR. If you feel some areas are not relevant add `N/A` as opposed to deleting those sections. PR's will not be reviewed if this template is not filled in.
- Fill out the [Pull Request Template](./.github/PULL_REQUEST_TEMPLATE.md) completely within the body of the PR. If you feel some areas are not relevant add `N/A` as opposed to deleting those sections. PRs will not be reviewed if this template is not filled in.
- If the PR is a user facing change and you're a Cypress team member that has logged into [ZenHub](https://www.zenhub.com/) and downloaded the [ZenHub for GitHub extension](https://www.zenhub.com/extension), set the release the PR is intended to ship in from the sidebar of the PR. Follow semantic versioning to select the intended release. This is used to generate the changelog for the release. If you don't tag a PR for release, it won't be mentioned in the changelog.
![Select release for PR](https://user-images.githubusercontent.com/1271364/135139641-657015d6-2dca-42d4-a4fb-16478f61d63f.png)
- Please check the "Allow edits from maintainers" checkbox when submitting your PR. This will make it easier for the maintainers to make minor adjustments, to help with tests or any other changes we may need.
![Allow edits from maintainers checkbox](https://user-images.githubusercontent.com/1271181/31393427-b3105d44-ada9-11e7-80f2-0dac51e3919e.png)
- All Pull Requests require a minimum of **two** approvals.
- After the PR is approved, the original contributor can merge the PR (if the original contributor has access).
- When you merge a PR into `develop`, select [**Squash and merge**](https://docs.github.com/en/github/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges#squash-and-merge-your-pull-request-commits). This will squash all commits into a single commit. *The only exception to squashing is when converting files to another language and there is a clear commit history needed to maintain from the file conversion.*
- When you merge a PR into `develop`, select [**Squash and merge**](https://docs.github.com/en/github/collaborating-with-pull-requests/incorporating-changes-from-a-pull-request/about-pull-request-merges#squash-and-merge-your-pull-request-commits). This will squash all commits into a single commit.
*The only exceptions to squashing are:*
1. When converting files to another language and there is a clear commit history needed to maintain from the file conversion.
2. When merging a `release/*` branch to `develop`. Individual PRs were already squashed when they were merged to the release branch, and we want that history intact on develop.
### Write Some Tests
If you are adding a new feature or fixing a regression, ensure you add tests for it. Broadly speaking, there are three categories of tests you might consider:
If you are adding a new feature or fixing a regression, ensure you add tests for it. Broadly speaking, there are four categories of tests you might consider:
1. Unit test. Those are inside of `test/unit`, if the package has them. These are the fastest and cheapest to execute.
2. E2E/Integration tests. Those are inside `cypress/e2e`, if the package has them. These are between Unit Tests and System Tests when it comes to speed of execution.
3. System Tests. Those go in the [`system-tests`](https://github.com/cypress-io/cypress/tree/develop/system-tests) directory. The README explains how they work. These are the slowest to run, so you generally only want to add a system-test if it's absolutely required (but don't let that discourage you; they are also the most realistic way to test Cypress).
1. Unit tests. Those are inside of `test/unit`, if the package has them. These are the fastest and cheapest to execute.
2. Component Tests. These are co-located with components in the `src` directory of UI-related packages. These test individual UI components in isolation. They can exhaustively test all expected variations of a component and are faster than E2E tests.
3. E2E/Integration tests. Those are inside `cypress/e2e`, if the package has them. These are between Unit Tests and System Tests when it comes to speed of execution.
4. System tests. Those go in the [`system-tests`](https://github.com/cypress-io/cypress/tree/develop/system-tests) directory. The README explains how they work. These are the slowest to run, so you generally only want to add a system-test if it's absolutely required (but don't let that discourage you; they are also the most realistic way to test Cypress).
When choosing what's most appropriate, consider:
@@ -511,6 +517,8 @@ When choosing what's most appropriate, consider:
- ease of debugging
- resilience to refactoring
It is also worth considering when a failure will be noticed. A unit or component test is likely to be run while the related code is being modified and provides very fast feedback. E2E tests and System Tests are more likely to only fail in CI since they are slower to run.
### Dependencies
We use [RenovateBot](https://renovatebot.com/) to automatically upgrade our dependencies. The bot uses the settings in [renovate.json](renovate.json) to maintain our [Update Dependencies](https://github.com/cypress-io/cypress/issues/3777) issue and open PRs. You can manually select a package to open a PR from our [Update Dependencies](https://github.com/cypress-io/cypress/issues/3777) issue.

View File

@@ -533,6 +533,7 @@ const util = {
la(is.unemptyString(varName), 'expected environment variable name, not', varName)
const configVarName = `npm_config_${varName}`
const configVarNameLower = configVarName.toLowerCase()
const packageConfigVarName = `npm_package_config_${varName}`
let result
@@ -545,6 +546,10 @@ const util = {
debug(`Using ${varName} from npm config`)
result = process.env[configVarName]
} else if (process.env.hasOwnProperty(configVarNameLower)) {
debug(`Using ${varName.toLowerCase()} from npm config`)
result = process.env[configVarNameLower]
} else if (process.env.hasOwnProperty(packageConfigVarName)) {
debug(`Using ${varName} from package.json config`)

View File

@@ -543,6 +543,11 @@ describe('util', () => {
expect(util.getEnv('CYPRESS_FOO')).to.eql('')
})
it('npm config set should work', () => {
process.env.npm_config_cypress_foo_foo = 'bazz'
expect(util.getEnv('CYPRESS_FOO_FOO')).to.eql('bazz')
})
it('throws on non-string name', () => {
expect(() => {
util.getEnv()

View File

@@ -197,6 +197,39 @@ declare namespace Cypress {
clear: (keys?: string[]) => void
}
// TODO: raise minimum required TypeScript version to 3.7
// and make this recursive
// https://github.com/cypress-io/cypress/issues/24875
type Storable =
| string
| number
| boolean
| null
| StorableObject
| StorableArray
interface StorableObject {
[key: string]: Storable
}
interface StorableArray extends Array<Storable> { }
type StorableRecord = Record<string, Storable>
interface OriginStorage {
origin: string
value: StorableRecord
}
interface Storages {
localStorage: OriginStorage[]
sessionStorage: OriginStorage[]
}
interface StorageByOrigin {
[key: string]: StorableRecord
}
type IsBrowserMatcher = BrowserName | Partial<Browser> | Array<BrowserName | Partial<Browser>>
interface ViewportPosition extends WindowPosition {
@@ -898,7 +931,35 @@ declare namespace Cypress {
clearCookies(options?: CookieOptions): Chainable<null>
/**
* Clear data in local storage.
* Get local storage for all origins.
*
* @see https://on.cypress.io/getalllocalstorage
*/
getAllLocalStorage(options?: Partial<Loggable>): Chainable<StorageByOrigin>
/**
* Clear local storage for all origins.
*
* @see https://on.cypress.io/clearalllocalstorage
*/
clearAllLocalStorage(options?: Partial<Loggable>): Chainable<null>
/**
* Get session storage for all origins.
*
* @see https://on.cypress.io/getallsessionstorage
*/
getAllSessionStorage(options?: Partial<Loggable>): Chainable<StorageByOrigin>
/**
* Clear session storage for all origins.
*
* @see https://on.cypress.io/clearallsessionstorage
*/
clearAllSessionStorage(options?: Partial<Loggable>): Chainable<null>
/**
* Clear data in local storage for the current origin.
* Cypress automatically runs this command before each test to prevent state from being
* shared across tests. You shouldn't need to use this command unless you're using it
* to clear localStorage inside a single test. Yields `localStorage` object.

View File

@@ -1084,3 +1084,29 @@ namespace CypressClearCookiesTests {
cy.clearCookies({ timeout: '10' }) // $ExpectError
cy.clearCookies({ domain: false }) // $ExpectError
}
namespace CypressLocalStorageTests {
cy.getAllLocalStorage().then((result) => {
result // $ExpectType StorageByOrigin
})
cy.getAllLocalStorage({ log: false })
cy.getAllLocalStorage({ log: 'true' }) // $ExpectError
cy.clearAllLocalStorage().then((result) => {
result // $ExpectType null
})
cy.clearAllLocalStorage({ log: false })
cy.clearAllLocalStorage({ log: 'true' }) // $ExpectError
cy.getAllSessionStorage().then((result) => {
result // $ExpectType StorageByOrigin
})
cy.getAllSessionStorage({ log: false })
cy.getAllSessionStorage({ log: 'true' }) // $ExpectError
cy.clearAllSessionStorage().then((result) => {
result // $ExpectType null
})
cy.clearAllSessionStorage({ log: false })
cy.clearAllSessionStorage({ log: 'true' }) // $ExpectError
}

View File

@@ -6,9 +6,15 @@ describe('run-all-specs', () => {
spec2: { relative: 'cypress/e2e/folder-a/spec-b.cy.js', name: 'runs folder-a/spec-b' },
spec3: { relative: 'cypress/e2e/folder-b/spec-a.cy.js', name: 'runs folder-b/spec-a' },
spec4: { relative: 'cypress/e2e/folder-b/spec-b.cy.js', name: 'runs folder-b/spec-b' },
spec5: { relative: 'folder-c/spec-a.cy.js', name: 'runs folder-c/spec-a' },
spec6: { relative: 'folder-c/spec-b.cy.js', name: 'runs folder-c/spec-b' },
}
const clickRunAllSpecs = (directory: string) => {
if (directory === 'all') {
return cy.findByTestId('run-all-specs-for-all').click()
}
const command = cy.get('[data-cy=spec-item-directory]').contains(directory)
return command.realHover().then(() => {
@@ -30,7 +36,7 @@ describe('run-all-specs', () => {
// Verify "Run All Specs" with sub-directory
const subDirectorySpecs = [ALL_SPECS.spec1, ALL_SPECS.spec2]
cy.get('[data-cy=sidebar-link-specs-page]').click()
cy.findByTestId('sidebar-link-specs-page').click()
clickRunAllSpecs('folder-a')
@@ -87,16 +93,16 @@ describe('run-all-specs', () => {
// Verify "Run All Specs" live-reload
cy.get('[data-cy=sidebar-link-specs-page]').click()
cy.findByLabelText('Search specs').clear()
cy.get('[data-cy=spec-list-file]').should('have.length', 4)
cy.get('[data-cy=spec-list-file]').should('have.length', 6)
clickRunAllSpecs('cypress/e2e')
clickRunAllSpecs('all')
cy.withCtx((ctx, { specs, runAllSpecsKey }) => {
expect(ctx.actions.project.launchProject).to.have.been.calledWith('e2e', undefined, runAllSpecsKey)
expect(ctx.project.runAllSpecs).to.include.members(specs.map((spec) => spec.relative))
}, { specs: Object.values(ALL_SPECS), runAllSpecsKey: RUN_ALL_SPECS_KEY })
cy.waitForSpecToFinish({ passCount: 4 })
cy.waitForSpecToFinish({ passCount: 6 })
for (const spec of Object.values(ALL_SPECS)) {
cy.get('.runnable-title').contains(spec.name)
@@ -111,6 +117,6 @@ describe('run-all-specs', () => {
await ctx.actions.file.writeFileInProject(spec.relative, newContent)
}, { spec: ALL_SPECS.spec1 })
cy.waitForSpecToFinish({ passCount: 3, failCount: 1 })
cy.waitForSpecToFinish({ passCount: 5, failCount: 1 })
})
})

View File

@@ -1,69 +0,0 @@
import { RUN_ALL_SPECS_KEY } from '@packages/types/src'
import { gql, useMutation, useQuery } from '@urql/vue'
import { computed, ComputedRef } from 'vue'
import { useRouter } from 'vue-router'
import { RunAllSpecsDocument, RunAllSpecs_ConfigDocument } from '../generated/graphql'
import { getSeparator, SpecTreeNode, UseCollapsibleTreeNode } from '../specs/tree/useCollapsibleTree'
type ResolvedConfig = { value: any, from: 'string', field: string }[]
gql`
query RunAllSpecs_Config {
currentProject {
id
config
currentTestingType
}
}
`
gql`
mutation RunAllSpecs ($specPath: String!, $runAllSpecs: [String!]!) {
setRunAllSpecs(runAllSpecs: $runAllSpecs)
launchOpenProject(specPath: $specPath) {
id
}
}
`
const isRunMode = window.__CYPRESS_MODE__ === 'run' && window.top === window
export function useRunAllSpecs (list: ComputedRef<{tree: UseCollapsibleTreeNode<SpecTreeNode>[]}>) {
const separator = getSeparator()
const router = useRouter()
const query = useQuery({ query: RunAllSpecs_ConfigDocument, pause: isRunMode })
const setRunAllSpecsMutation = useMutation(RunAllSpecsDocument)
return {
runAllSpecs: async (runAllSpecs: string[]) => {
await setRunAllSpecsMutation.executeMutation({ runAllSpecs, specPath: RUN_ALL_SPECS_KEY })
// Won't execute unless we are testing since the browser gets killed. In testing,
// we can stub `launchProject` to verify the functionality is working
router.push({ path: '/specs/runner', query: { file: RUN_ALL_SPECS_KEY } })
},
isRunAllSpecsAllowed: computed(() => {
const isE2E = query.data.value?.currentProject?.currentTestingType === 'e2e'
const config: ResolvedConfig = query.data.value?.currentProject?.config || []
const hasExperiment = config.find(({ field, value }) => field === 'experimentalRunAllSpecs' && value === true)
return Boolean(isE2E && hasExperiment)
}),
directoryChildren: computed(() => {
return list.value.tree.reduce<Record<string, string[]>>((acc, node) => {
if (!node.isLeaf) {
acc[node.id] = []
} else {
Object.keys(acc).forEach((dir) => {
if (node.id.startsWith(dir) && node.id.replace(dir, '').startsWith(separator)) {
acc[dir].push(node.id)
}
})
}
return acc
}, {})
}),
}
}

View File

@@ -31,6 +31,7 @@ import SpecRunnerContainerOpenMode from '../../runner/SpecRunnerContainerOpenMod
import SpecRunnerContainerRunMode from '../../runner/SpecRunnerContainerRunMode.vue'
import { useEventManager } from '../../runner/useEventManager'
import { useSpecStore } from '../../store'
import { isRunMode } from '@packages/frontend-shared/src/utils/isRunMode'
gql`
query SpecPageContainer {
@@ -59,18 +60,14 @@ subscription Runner_ConfigChange {
}
`
const isRunMode = window.__CYPRESS_MODE__ === 'run'
// subscriptions are used to trigger live updates without
// reloading the page.
// this is only useful in open mode - in run mode, we don't
// use GraphQL, so we pause the
// subscriptions so they never execute.
const shouldPauseSubscriptions = isRunMode && window.top === window
useSubscription({
query: SpecPageContainer_SpecsChangeDocument,
pause: shouldPauseSubscriptions,
pause: isRunMode,
})
// in run mode, we are not using GraphQL or urql
@@ -80,7 +77,7 @@ useSubscription({
// requests, which is what we want.
const query = useQuery({
query: SpecPageContainerDocument,
pause: shouldPauseSubscriptions,
pause: isRunMode,
})
let initialLoad = true

View File

@@ -8,12 +8,12 @@
</template>
<script lang="ts" setup>
import { isRunMode } from '@packages/frontend-shared/src/utils/isRunMode'
import { computed } from 'vue'
import { useScreenshotStore } from '../../store'
import { runnerConstants } from '../runner-constants'
const screenshotStore = useScreenshotStore()
const isRunMode = window.__CYPRESS_MODE__ === 'run'
const style = computed(() => {
if (screenshotStore.isScreenshotting) {

View File

@@ -1,3 +1,4 @@
import { isRunMode } from '@packages/frontend-shared/src/utils/isRunMode'
import { useWindowSize } from '@vueuse/core'
import { computed, ref, watchEffect } from 'vue'
import { usePreferences } from '../composables/usePreferences'
@@ -97,7 +98,7 @@ export const useRunnerStyle = () => {
return {
viewportStyle,
windowWidth: computed(() => {
if (window.__CYPRESS_MODE__ === 'run') {
if (isRunMode) {
return windowWidth.value
}

View File

@@ -13,15 +13,27 @@ describe('<InlineRunAllSpecs/>', () => {
})
it('renders component correctly', () => {
cy.findByTestId('tooltip').children().should('have.length', 1)
cy.findByTestId('run-all-specs-for-cypress/e2e').children().should('have.length', 1)
cy.findByTestId('play-button').should('be.visible')
})
it('provides expected tooltip content', () => {
cy.findByTestId('tooltip-content').should('not.exist')
cy.findByTestId('tooltip').realHover().then(() => {
cy.findByTestId('run-all-specs-for-cypress/e2e').realHover().then(() => {
cy.findByTestId('tooltip-content').should('contain.text', 'Run 40 specs')
})
})
})
it('disables button when no specs are available', () => {
cy.mount(() => {
return (
<div class="flex justify-center">
<InlineRunAllSpecs specNumber={0} directory='cypress/e2e' />
</div>
)
})
cy.findByTestId('run-all-specs-button').should('be.disabled')
})
})

View File

@@ -1,16 +1,20 @@
<template>
<Tooltip
placement="right"
class="h-full truncate"
data-cy="tooltip"
:data-cy="`run-all-specs-for-${directory}`"
>
<button @click.stop="emits('runAllSpecs')">
<button
class="flex h-full w-full items-center justify-center"
data-cy="run-all-specs-button"
:disabled="specNumber === 0"
@click.stop="emits('runAllSpecs')"
>
<IconActionPlaySmall
size="16"
stroke-color="gray-700"
:stroke-color="grayscale ? 'gray-200' : 'gray-700'"
fill-color="transparent"
hocus-stroke-color="indigo-500"
hocus-fill-color="indigo-100"
:hocus-stroke-color="grayscale ? undefined : 'indigo-500'"
:hocus-fill-color="grayscale ? undefined : 'indigo-100'"
class="inline-flex align-text-bottom"
data-cy="play-button"
/>
@@ -22,7 +26,7 @@
class="font-normal text-sm inline-flex"
data-cy="tooltip-content"
>
{{ t('specPage.runAllSpecs', specNumber) }}
{{ t('specPage.runSelectedSpecs', specNumber) }}
</span>
</template>
</Tooltip>
@@ -38,6 +42,7 @@ const { t } = useI18n()
defineProps<{
specNumber: number
directory: string
grayscale?: boolean
}>()
const emits = defineEmits<{

View File

@@ -4,23 +4,6 @@ import { defaultMessages } from '@cy/i18n'
let specs: Array<any> = []
const hoverRunAllSpecs = (directory?: string, specNumber?: number) => {
let command
if (directory) {
command = cy.contains('[data-cy=directory-item]', directory)
} else {
command = cy.get('[data-cy=directory-item]').first()
}
return command.realHover().then(() => {
cy.get('[data-cy=play-button]').should('exist')
cy.get('[data-cy=run-all-specs]').realHover().then(() => {
cy.get('[data-cy=tooltip-content]').should('contain.text', `Run ${specNumber} spec`)
})
})
}
describe('InlineSpecList', () => {
const mountInlineSpecList = ({ specFilter, experimentalRunAllSpecs }: {specFilter?: string, experimentalRunAllSpecs?: boolean} = {}) => cy.mountFragment(Specs_InlineSpecListFragmentDoc, {
onResult: (ctx) => {
@@ -207,10 +190,29 @@ describe('InlineSpecList', () => {
})
describe('Run all Specs', () => {
const hoverRunAllSpecs = (directory: string, specNumber: number) => {
let command = cy.contains('[data-cy=directory-item]', directory)
return command.realHover().then(() => {
cy.get('[data-cy=play-button]').should('exist')
cy.get(`[data-cy="run-all-specs-for-${directory}"]`).realHover().then(() => {
cy.get('[data-cy=tooltip-content]').should('contain.text', `Run ${specNumber} spec`)
})
})
}
beforeEach(() => {
cy.fixture('found-specs').then((foundSpecs) => specs = foundSpecs)
})
it('does not show feature unless experimentalRunAllSpecs is enabled', () => {
mountInlineSpecList({ experimentalRunAllSpecs: false })
cy.findByTestId('run-all-specs-for-all').should('not.exist')
cy.contains('[data-cy=directory-item]', 'src').realHover()
cy.findByTestId('run-all-specs-for-src').should('not.exist')
})
it('displays runAllSpecs when hovering over a spec-list directory row', () => {
mountInlineSpecList({ experimentalRunAllSpecs: true })
hoverRunAllSpecs('src', 4)
@@ -218,7 +220,7 @@ describe('InlineSpecList', () => {
it('checks if functionality works after a search', () => {
mountInlineSpecList({ experimentalRunAllSpecs: true, specFilter: 'B' })
hoverRunAllSpecs('src', 1)
hoverRunAllSpecs('src/components', 1)
})
})
})

View File

@@ -9,7 +9,9 @@
<InlineSpecListHeader
v-model:specFilterModel="specFilterModel"
:result-count="specs.length"
:is-run-all-specs-allowed="runAllSpecsStore.isRunAllSpecsAllowed"
@newSpec="showModal = true"
@run-all-specs="runAllSpecsStore.runAllSpecs"
/>
<InlineSpecListTree
:specs="specs"
@@ -31,6 +33,7 @@ import CreateSpecModal from './CreateSpecModal.vue'
import { fuzzySortSpecs, makeFuzzyFoundSpec, useCachedSpecs } from './spec-utils'
import type { FuzzyFoundSpec } from './tree/useCollapsibleTree'
import { useSpecFilter } from '../composables/useSpecFilter'
import { useRunAllSpecsStore } from '../store/run-all-specs-store'
gql`
fragment SpecNode_InlineSpecList on Spec {
@@ -80,4 +83,6 @@ const specs = computed<FuzzyFoundSpec[]>(() => {
return fuzzySortSpecs(specs, debouncedSpecFilterModel.value)
})
const runAllSpecsStore = useRunAllSpecsStore()
</script>

View File

@@ -1,29 +1,32 @@
import InlineSpecListHeader from './InlineSpecListHeader.vue'
import { ref } from 'vue'
import { defaultMessages } from '@cy/i18n'
import { defineStore } from 'pinia'
describe('InlineSpecListHeader', () => {
const mountWithResultCount = (resultCount = 0) => {
const mountWithProps = (props: {resultCount?: number, isRunAllSpecsAllowed?: boolean} = {}) => {
const specFilterModel = ref('')
const onNewSpec = cy.spy().as('new-spec')
cy.wrap(specFilterModel).as('specFilterModel')
const methods = {
const propsWithDefaults = {
resultCount: props.resultCount ?? 0,
isRunAllSpecsAllowed: props.isRunAllSpecsAllowed ?? false,
'onUpdate:specFilterModel': (val: string) => {
specFilterModel.value = val
},
onNewSpec,
onNewSpec: cy.spy().as('new-spec'),
onRunAllSpecs: cy.spy().as('run-all-specs'),
}
cy.wrap(specFilterModel).as('specFilterModel')
cy.mount(() =>
(<div class="bg-gray-1000">
<InlineSpecListHeader {...methods} specFilterModel={specFilterModel.value} resultCount={resultCount} />
<InlineSpecListHeader {...propsWithDefaults} specFilterModel={specFilterModel.value} />
</div>))
}
it('should allow search', () => {
mountWithResultCount(0)
mountWithProps({ resultCount: 0 })
const searchString = 'my/component.cy.tsx'
cy.findByLabelText(defaultMessages.specPage.searchPlaceholder)
@@ -33,7 +36,7 @@ describe('InlineSpecListHeader', () => {
})
it('should emit add spec', () => {
mountWithResultCount(0)
mountWithProps({ resultCount: 0 })
cy.findAllByLabelText(defaultMessages.specPage.newSpecButton)
.click()
.get('@new-spec')
@@ -41,7 +44,7 @@ describe('InlineSpecListHeader', () => {
})
it('clears search field when clear button is clicked', () => {
mountWithResultCount(0)
mountWithProps({ resultCount: 0 })
cy.findByTestId('clear-search-button')
.should('not.exist')
@@ -56,14 +59,37 @@ describe('InlineSpecListHeader', () => {
})
it('exposes the result count correctly to assistive tech', () => {
mountWithResultCount(0)
mountWithProps({ resultCount: 0 })
cy.contains('No matches')
.should('have.class', 'sr-only')
.and('have.attr', 'aria-live', 'polite')
mountWithResultCount(1)
mountWithProps({ resultCount: 1 })
cy.contains('1 match').should('have.class', 'sr-only')
mountWithResultCount(100)
mountWithProps({ resultCount: 100 })
cy.contains('100 matches').should('have.class', 'sr-only')
})
it('renders "Run All Specs" button with flag and emits on click', () => {
const EXPECTED_SPEC_COUNT = 2
// make a small store to simulate some specs existing
// without touching any of the gql used by the real store
const useRunAllSpecsStore = defineStore('runAllSpecs', {
state: () => ({ allSpecsRef: ref(Array(EXPECTED_SPEC_COUNT)) }),
})
useRunAllSpecsStore()
mountWithProps({ isRunAllSpecsAllowed: true })
cy.percySnapshot()
cy.get('[data-cy=run-all-specs-for-all]').as('run-all-btn').realHover()
cy.contains(`Run ${EXPECTED_SPEC_COUNT} specs`).should('be.visible')
cy.percySnapshot('with tooltip')
cy.get('@run-all-btn').click()
cy.get('@run-all-specs').should('have.been.called')
})
})

View File

@@ -1,14 +1,12 @@
<template>
<div
class="border-b-1 border-gray-900 h-64px mx-16px grid gap-8px grid-cols-[minmax(0,1fr),24px] pointer-cursor items-center"
class="border-b-1 border-gray-900 h-64px mx-16px auto-cols-max grid grid-flow-col gap-8px grid-cols-[minmax(0,1fr)] pointer-cursor items-center"
>
<div
class="relative items-center"
@click="input?.focus()"
>
<div
class="flex h-full inset-y-0 w-32px absolute items-center pointer-events-none"
>
<div class="flex h-full inset-y-0 w-32px absolute items-center pointer-events-none">
<i-cy-magnifying-glass_x16
:class="inputFocused ? 'icon-dark-indigo-300' : 'icon-dark-gray-800'"
class="icon-light-gray-1000"
@@ -17,15 +15,7 @@
<input
id="inline-spec-list-header-search"
ref="input"
class="
font-light
outline-none
bg-gray-1000
border-0
px-6
placeholder-gray-700
text-gray-500
"
class="font-light outline-none bg-gray-1000 border-0 px-6 placeholder-gray-700 text-gray-500"
:class="inputFocused || props.specFilterModel.length ? 'w-full' : 'w-16px'"
:value="props.specFilterModel"
type="search"
@@ -60,26 +50,36 @@
/>
</button>
</div>
<button
class="
rounded-md flex
outline-none
border-1
border-gray-900
h-24px
w-24px
duration-300
hocus-default
items-center
justify-center
hocus:ring-0
hocus:border-indigo-300
"
:aria-label="t('specPage.newSpecButton')"
@click="emit('newSpec')"
<Tooltip
placement="right"
data-cy="tooltip"
>
<i-cy-add-small_x16 class="icon-light-gray-50 icon-dark-gray-200" />
</button>
<button
class="rounded-md flex outline-none border-1 border-gray-900 h-24px w-24px duration-300 hocus-default items-center justify-center hocus:ring-0 hocus:border-indigo-300"
:aria-label="t('specPage.newSpecButton')"
@click="emit('newSpec')"
>
<i-cy-add-small_x16 class="icon-light-gray-50 icon-dark-gray-200" />
</button>
<template
#popper
>
<span
class="font-normal text-sm inline-flex"
data-cy="tooltip-content"
>
{{ t('specPage.newSpecButton') }}
</span>
</template>
</Tooltip>
<InlineRunAllSpecs
v-if="isRunAllSpecsAllowed"
:spec-number="runAllSpecsStore.allSpecsRef.length"
directory="all"
grayscale
class="rounded-md flex outline-none border-1 border-gray-900 h-24px w-24px duration-300 hocus-default items-center justify-center hocus:ring-0 hocus:border-indigo-300"
@runAllSpecs="emit('runAllSpecs')"
/>
<div
class="sr-only"
aria-live="polite"
@@ -92,18 +92,25 @@
<script lang="ts" setup>
import { ref } from 'vue'
import { useI18n } from '@cy/i18n'
import InlineRunAllSpecs from './InlineRunAllSpecs.vue'
import Tooltip from '@packages/frontend-shared/src/components/Tooltip.vue'
import { useRunAllSpecsStore } from '../store/run-all-specs-store'
const { t } = useI18n()
const props = defineProps<{
specFilterModel: string
resultCount: number
isRunAllSpecsAllowed: boolean
}>()
const emit = defineEmits<{
(e: 'update:specFilterModel', specFilterModel: string): void
(e: 'newSpec'): void
(e: 'runAllSpecs'): void
}>()
const runAllSpecsStore = useRunAllSpecsStore()
const inputFocused = ref(false)
const input = ref<HTMLInputElement>()

View File

@@ -55,12 +55,11 @@
>
<template #run-all-specs>
<InlineRunAllSpecs
v-if="isRunAllSpecsAllowed"
data-cy="run-all-specs"
v-if="runAllSpecsStore.isRunAllSpecsAllowed"
:directory="row.data.name"
class="opacity-0 run-all"
:spec-number="directoryChildren[row.data.id].length"
@runAllSpecs="onRunAllSpecs(row.data.id)"
class="flex h-full opacity-0 run-all justify-center items-center"
:spec-number="runAllSpecsStore.directoryChildren[row.data.id].length"
@runAllSpecs="() => runAllSpecsStore.runSelectedSpecs(row.data.id)"
/>
</template>
</DirectoryItem>
@@ -81,7 +80,7 @@ import { useVirtualList } from './tree/useVirtualList'
import { useVirtualListNavigation } from './tree/useVirtualListNavigation'
import { useStudioStore } from '../store/studio-store'
import InlineRunAllSpecs from './InlineRunAllSpecs.vue'
import { useRunAllSpecs } from '../composables/useRunAllSpecs'
import { useRunAllSpecsStore } from '../store/run-all-specs-store'
const props = defineProps<{
specs: FuzzyFoundSpec[]
@@ -166,11 +165,11 @@ const resetFocusIfNecessary = (row, index) => {
}
}
const { runAllSpecs, isRunAllSpecsAllowed, directoryChildren } = useRunAllSpecs(collapsible)
const runAllSpecsStore = useRunAllSpecsStore()
function onRunAllSpecs (rowId: string) {
runAllSpecs(directoryChildren.value[rowId])
}
watch(collapsible, () => {
runAllSpecsStore.setRunAllSpecsData(collapsible.value.tree)
}, { immediate: true })
</script>

View File

@@ -293,6 +293,14 @@ describe('<SpecsList />', { keystrokeDelay: 0 }, () => {
})
}
it('does not show feature unless experimentalRunAllSpecs is enabled', () => {
mountWithTestingType({ experimentalRunAllSpecs: false })
cy.contains('button', 'Run all specs').should('not.exist')
cy.contains('[data-cy=spec-item-directory]', '__test__').realHover()
cy.contains('button', 'Run 5 specs').should('not.exist')
})
it('displays runAllSpecs when hovering over a spec-list directory row', () => {
mountWithTestingType({ experimentalRunAllSpecs: true })
hoverRunAllSpecs('__test__', 5)

View File

@@ -32,11 +32,20 @@
:class="tableGridColumns"
>
<div
class="flex items-center justify-between"
class="flex items-center"
data-cy="specs-testing-type-header"
>
{{ props.gql.currentProject?.currentTestingType === 'component' ?
t('specPage.componentSpecsHeader') : t('specPage.e2eSpecsHeader') }}
<span>
{{ props.gql.currentProject?.currentTestingType === 'component'
? t('specPage.componentSpecsHeader')
: t('specPage.e2eSpecsHeader') }}
</span>
<SpecsRunAllSpecs
v-if="runAllSpecsStore.isRunAllSpecsAllowed"
:spec-number="runAllSpecsStore.allSpecsRef.length"
directory="all"
@runAllSpecs="runAllSpecsStore.runAllSpecs"
/>
</div>
<div class="flex items-center justify-between truncate">
<LastUpdatedHeader :is-git-available="isGitAvailable" />
@@ -111,16 +120,15 @@
:depth="row.data.depth - 2"
:style="{ paddingLeft: `${(row.data.depth - 2) * 10}px` }"
:indexes="row.data.highlightIndexes"
:is-run-all-specs-allowed="isRunAllSpecsAllowed"
:aria-controls="getIdIfDirectory(row)"
@toggle="() => row.data.toggle()"
>
<SpecsRunAllSpecs
v-if="isRunAllSpecsAllowed"
v-if="runAllSpecsStore.isRunAllSpecsAllowed"
:directory="row.data.name"
class="opacity-0 run-all"
:spec-number="directoryChildren[row.data.id].length"
@runAllSpecs="onRunAllSpecs(row.data.id)"
:spec-number="runAllSpecsStore.directoryChildren[row.data.id].length"
@runAllSpecs="() => runAllSpecsStore.runSelectedSpecs(row.data.id)"
/>
</RowDirectory>
</template>
@@ -207,7 +215,7 @@ import { useSpecFilter } from '../composables/useSpecFilter'
import { useRequestAccess } from '../composables/useRequestAccess'
import { useLoginConnectStore } from '@packages/frontend-shared/src/store/login-connect-store'
import SpecsRunAllSpecs from './SpecsRunAllSpecs.vue'
import { useRunAllSpecs } from '../composables/useRunAllSpecs'
import { useRunAllSpecsStore } from '../store/run-all-specs-store'
const { openLoginConnectModal } = useLoginConnectStore()
@@ -427,11 +435,11 @@ const { refetchFailedCloudData } = useCloudSpecData(
props.gql.currentProject?.specs as SpecsListFragment[] || [],
)
const { runAllSpecs, isRunAllSpecsAllowed, directoryChildren } = useRunAllSpecs(collapsible)
const runAllSpecsStore = useRunAllSpecsStore()
function onRunAllSpecs (rowId: string) {
runAllSpecs(directoryChildren.value[rowId])
}
watch(collapsible, () => {
runAllSpecsStore.setRunAllSpecsData(collapsible.value.tree)
}, { immediate: true })
</script>

View File

@@ -18,7 +18,7 @@
class="font-normal text-sm"
data-cy="run-all-specs-text"
>
{{ t('specPage.runAllSpecs', specNumber) }}
{{ t('specPage.runSelectedSpecs', specNumber) }}
</span>
</button>
</template>

View File

@@ -0,0 +1,97 @@
import { RUN_ALL_SPECS_KEY } from '@packages/types/src'
import { gql, useMutation, useQuery } from '@urql/vue'
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
import { useRouter } from 'vue-router'
import { RunAllSpecsDataDocument, RunAllSpecsDocument } from '../generated/graphql'
import { getSeparator, SpecTreeNode, UseCollapsibleTreeNode } from '../specs/tree/useCollapsibleTree'
import { isRunMode } from '@packages/frontend-shared/src/utils/isRunMode'
type ResolvedConfig = Array<{ value: any, from: string, field: string }>
gql`
query RunAllSpecsData {
currentProject {
id
config
currentTestingType
}
}
`
gql`
mutation RunAllSpecs ($specPath: String!, $runAllSpecs: [String!]!) {
setRunAllSpecs(runAllSpecs: $runAllSpecs)
launchOpenProject(specPath: $specPath) {
id
}
}
`
// TODO: This is a "setup store" - see https://pinia.vuejs.org/core-concepts/#setup-stores
// Can we make it an "options store" like the others? https://pinia.vuejs.org/core-concepts/#option-stores
export const useRunAllSpecsStore = defineStore('runAllSpecs', () => {
const allSpecsRef = ref<string[]>([])
const directoryChildrenRef = ref<Record<string, string[]>>({})
const separator = getSeparator()
const router = useRouter()
const query = useQuery({ query: RunAllSpecsDataDocument, pause: isRunMode })
const setRunAllSpecsMutation = useMutation(RunAllSpecsDocument)
async function runSpecs (runAllSpecs: string[]) {
await setRunAllSpecsMutation.executeMutation({ runAllSpecs, specPath: RUN_ALL_SPECS_KEY })
// Won't execute unless we are testing since the browser gets killed. In testing,
// we can stub `launchProject` to verify the functionality is working
router.push({ path: '/specs/runner', query: { file: RUN_ALL_SPECS_KEY } })
}
async function runAllSpecs () {
await runSpecs(allSpecsRef.value)
}
async function runSelectedSpecs (dir: string) {
await runSpecs(directoryChildrenRef.value[dir])
}
function setRunAllSpecsData (tree: UseCollapsibleTreeNode<SpecTreeNode>[]) {
const allSpecs: string[] = []
const directoryChildren: Record<string, string[]> = {}
for (const { id, isLeaf } of tree) {
if (!isLeaf) {
directoryChildren[id] = []
} else {
allSpecs.push(id)
Object.keys(directoryChildren).forEach((dir) => {
if (id.startsWith(dir) && id.replace(dir, '').startsWith(separator)) {
directoryChildren[dir].push(id)
}
})
}
}
allSpecsRef.value = allSpecs
directoryChildrenRef.value = directoryChildren
}
const isRunAllSpecsAllowed = computed(() => {
const isE2E = query.data.value?.currentProject?.currentTestingType === 'e2e'
const config: ResolvedConfig = query.data.value?.currentProject?.config || []
const hasExperiment = config.some(({ field, value }) => field === 'experimentalRunAllSpecs' && value === true)
return (isE2E && hasExperiment)
})
return {
isRunAllSpecsAllowed,
directoryChildren: directoryChildrenRef,
runAllSpecs,
allSpecsRef,
runSelectedSpecs,
setRunAllSpecsData,
}
})

View File

@@ -2,6 +2,7 @@ import type { ParsedPath } from 'path'
import type { CodeGenType } from '@packages/graphql/src/gen/nxs.gen'
import type { WizardFrontendFramework } from '@packages/scaffold-config'
import fs from 'fs-extra'
import { upperFirst } from 'lodash'
import path from 'path'
import { getDefaultSpecFileName } from '../sources/migration/utils'
import { toPosix } from '../util'
@@ -81,7 +82,7 @@ export class SpecOptions {
}
private async getFrameworkComponentOptions () {
const componentName = this.parsedPath.name
const componentName = this.buildComponentNameFromFilename(this.parsedPath.name)
const extension = await this.getVueExtension()
@@ -138,11 +139,25 @@ export class SpecOptions {
return foundSpecExtension || ''
}
private buildComponentNameFromFilename (fileNameWithoutExt: string): string {
const sanitizedName = fileNameWithoutExt
// Remove any characters from the filename that aren't allowed within a JS variable name (but leave periods and hyphens)
.replaceAll(/[^a-z_\d$.-]/gi, '')
// Remove any groupings of multiple periods (eg, '...all') but leave single periods alone
.replaceAll(/[.]{2,}/g, '')
// Convert period- and hyphen-delimited portions to PascalCase
// eg, 'test.page.ts' => 'TestPage', 'about.component.vue' => 'AboutComponent'
return sanitizedName.split(/[-.]/g)
.map(upperFirst)
.join('')
}
private buildComponentSpecFilename (specExt: string, filePath?: ParsedPath) {
const { dir, base, ext } = filePath || this.parsedPath
const cyWithExt = this.getSpecExtension(filePath) + ext
const name = base.slice(0, base.indexOf('.'))
const name = base.slice(0, -cyWithExt.length)
const finalExtension = filePath ? cyWithExt : specExt

View File

@@ -242,6 +242,37 @@ describe('spec-options', () => {
expect(result.codeGenType).to.eq('e2e')
expect(result.fileName).to.eq(`TestName.foo.bar-copy-2.js`)
})
context('file name contains special characters', async () => {
[
{ condition: 'braces', fileName: '[...MyComponent].vue', expectedFileName: '[...MyComponent].cy.js', expectedComponentName: 'MyComponent' },
{ condition: 'hyphens', fileName: 'my-component.vue', expectedFileName: 'my-component.cy.js', expectedComponentName: 'MyComponent' },
{ condition: 'parentheses', fileName: 'My(Component).js', expectedFileName: 'My(Component).cy.js', expectedComponentName: 'MyComponent' },
{ condition: 'period-separated', fileName: 'my.component.js', expectedFileName: 'my.component.cy.js', expectedComponentName: 'MyComponent' },
{ condition: 'dollar', fileName: '$MyComponent.js', expectedFileName: '$MyComponent.cy.js', expectedComponentName: '$MyComponent' },
{ condition: 'underscores', fileName: 'My_Component.js', expectedFileName: 'My_Component.cy.js', expectedComponentName: 'My_Component' },
{ condition: 'mixed period- and hypen-delimited', fileName: 'about-us.component.js', expectedFileName: 'about-us.component.cy.js', expectedComponentName: 'AboutUsComponent' },
].forEach(({ condition, fileName, expectedFileName, expectedComponentName }) => {
it(`generates options for ${condition}`, async () => {
const testSpecOptions = new SpecOptions({
currentProject: 'path/to/myProject',
codeGenPath: `${tmpPath}/${fileName}`,
codeGenType: 'component',
isDefaultSpecPattern: true,
specPattern: [defaultSpecPattern.component],
framework: WIZARD_FRAMEWORKS[1],
})
await fs.outputFile(`${tmpPath}/${fileName}`, '// foo')
const result = await testSpecOptions.getCodeGenOptions()
expect(result.codeGenType).to.eq('component')
expect(result.fileName).to.eq(expectedFileName)
expect(result['componentName']).to.eq(expectedComponentName)
})
})
})
})
})
})

View File

@@ -3,12 +3,10 @@ const $Cypress = require('../../../../src/cypress').default
describe('src/cy/commands/sessions/manager.ts', () => {
let CypressInstance
let baseUrl
beforeEach(function () {
// @ts-ignore
CypressInstance = new $Cypress()
baseUrl = Cypress.config('baseUrl')
})
it('creates SessionsManager instance', () => {
@@ -168,70 +166,6 @@ describe('src/cy/commands/sessions/manager.ts', () => {
})
})
describe('.mapOrigins()', () => {
it('maps when requesting all origins', async () => {
const sessionsManager = new SessionsManager(CypressInstance, cy)
const allOrigins = ['https://example.com', baseUrl, 'http://foobar.com', 'http://foobar.com']
const sessionsSpy = cy.stub(sessionsManager, 'getAllHtmlOrigins').resolves(allOrigins)
const origins = await sessionsManager.mapOrigins('*')
expect(origins).to.deep.eq(['https://example.com', baseUrl, 'http://foobar.com'])
expect(sessionsSpy).to.be.calledOnce
})
it('maps when requesting the current origin', async () => {
const sessionsManager = new SessionsManager(CypressInstance, cy)
const sessionsSpy = cy.stub(sessionsManager, 'getAllHtmlOrigins')
const origins = await sessionsManager.mapOrigins('currentOrigin')
expect(origins).to.deep.eq([baseUrl])
expect(sessionsSpy).not.to.be.called
})
it('maps when requesting a specific origin', async () => {
const sessionsManager = new SessionsManager(CypressInstance, cy)
const sessionsSpy = cy.stub(sessionsManager, 'getAllHtmlOrigins')
const origins = await sessionsManager.mapOrigins('https://example.com/random_page?1')
expect(origins).to.deep.eq(['https://example.com'])
expect(sessionsSpy).not.to.be.called
})
it('maps when requesting a list of origins', async () => {
const sessionsManager = new SessionsManager(CypressInstance, cy)
const allOrigins = ['https://example.com', baseUrl, 'http://foobar.com', 'http://foobar.com']
const sessionsSpy = cy.stub(sessionsManager, 'getAllHtmlOrigins').resolves(allOrigins)
const origins = await sessionsManager.mapOrigins(['*', 'https://other.com'])
expect(origins).to.deep.eq(['https://example.com', baseUrl, 'http://foobar.com', 'https://other.com'])
expect(sessionsSpy).to.be.calledOnce
})
})
// TODO:
describe('._setStorageOnOrigins()', () => {})
it('.getAllHtmlOrigins()', async () => {
const storedOrigins = {
'https://example.com': {},
'https://foobar.com': {},
}
storedOrigins[`${baseUrl}`] = {}
const cypressSpy = cy.stub(CypressInstance, 'backend').callThrough().withArgs('get:rendered:html:origins').resolves(storedOrigins)
const sessionsManager = new SessionsManager(CypressInstance, cy)
const origins = await sessionsManager.getAllHtmlOrigins()
expect(cypressSpy).have.been.calledOnce
expect(origins).to.have.lengthOf(3)
expect(origins).to.deep.eq(['https://example.com', 'https://foobar.com', baseUrl])
})
describe('.sessions', () => {
it('sessions.defineSession()', () => {
const sessionsManager = new SessionsManager(CypressInstance, cy)
@@ -273,17 +207,27 @@ describe('src/cy/commands/sessions/manager.ts', () => {
expect(window.localStorage).of.have.lengthOf(1)
expect(window.sessionStorage).of.have.lengthOf(1)
const specWindow = {}
CypressInstance.log = cy.stub()
CypressInstance.state = cy.stub()
CypressInstance.state.withArgs('specWindow').returns(specWindow)
const storedOrigins = {}
cy.stub(CypressInstance, 'backend')
.callThrough()
.withArgs('get:rendered:html:origins')
.resolves(storedOrigins)
const sessionsManager = new SessionsManager(CypressInstance, {
state: () => true,
})
const clearStorageSpy = cy.stub(sessionsManager.sessions, 'clearStorage')
const clearCookiesSpy = cy.stub(sessionsManager.sessions, 'clearCookies')
await sessionsManager.sessions.clearCurrentSessionData()
expect(clearStorageSpy).to.be.calledOnce
expect(clearCookiesSpy).to.be.calledOnce
expect(window.localStorage).of.have.lengthOf(0)
expect(window.sessionStorage).of.have.lengthOf(0)
@@ -292,8 +236,8 @@ describe('src/cy/commands/sessions/manager.ts', () => {
it('does not log message when setting up tests', async () => {
// Unable to cleanly mock localStorage or sessionStorage on Firefox,
// so add dummy values and ensure they are cleared as expected.
// https://bugzilla.mozilla.org/show_bug.cgi?id=1141698
// so add dummy values and ensure they are cleared as expected.
// https://bugzilla.mozilla.org/show_bug.cgi?id=1141698
window.localStorage.foo = 'bar'
window.sessionStorage.jazzy = 'music'
@@ -301,16 +245,14 @@ describe('src/cy/commands/sessions/manager.ts', () => {
expect(window.sessionStorage).of.have.lengthOf(1)
CypressInstance.log = cy.stub()
const sessionsManager = new SessionsManager(CypressInstance, {
const sessionsManager = new SessionsManager(Cypress, {
state: () => false,
})
const clearStorageSpy = cy.stub(sessionsManager.sessions, 'clearStorage')
const clearCookiesSpy = cy.stub(sessionsManager.sessions, 'clearCookies')
await sessionsManager.sessions.clearCurrentSessionData()
expect(clearStorageSpy).to.be.calledOnce
expect(clearCookiesSpy).to.be.calledOnce
expect(window.localStorage).of.have.lengthOf(0)
expect(window.sessionStorage).of.have.lengthOf(0)
@@ -373,19 +315,18 @@ describe('src/cy/commands/sessions/manager.ts', () => {
expect(cypressSpy).to.be.calledOnceWith('clear:cookies', cookies)
})
it('sessions.getCurrentSessionData', async () => {
const sessionsManager = new SessionsManager(CypressInstance, () => {})
const getStorageSpy = cy.stub(sessionsManager.sessions, 'getStorage').resolves({ localStorage: [] })
it('sessions.getCurrentSessionData()', async () => {
const sessionsManager = new SessionsManager(Cypress, () => {})
const cookiesSpy = cy.stub(sessionsManager.sessions, 'getCookies').resolves([{ id: 'cookie' }])
const sessData = await sessionsManager.sessions.getCurrentSessionData()
expect(sessData).to.deep.eq({
expect(sessData).to.deep.equal({
localStorage: [],
sessionStorage: [],
cookies: [{ id: 'cookie' }],
})
expect(getStorageSpy).to.be.calledOnce
expect(cookiesSpy).to.be.calledOnce
})
@@ -398,14 +339,5 @@ describe('src/cy/commands/sessions/manager.ts', () => {
expect(cypressSpy).to.be.calledOnceWith('get:session', 'session_1')
})
// TODO:
describe('sessions.getStorage', () => {})
// TODO:
describe('sessions.clearStorage', () => {})
// TODO:
describe('sessions.setStorage', () => {})
})
})

View File

@@ -0,0 +1,87 @@
import { getAllHtmlOrigins, mapOrigins } from '../../../../src/cy/commands/sessions/origins'
const $Cypress = require('../../../../src/cypress').default
describe('src/cy/commands/sessions/origins', () => {
let CypressInstance
let baseUrl
beforeEach(function () {
// @ts-ignore
CypressInstance = new $Cypress()
baseUrl = Cypress.config('baseUrl')
})
describe('mapOrigins()', () => {
it('returns unique origins when requesting all origins', async () => {
const storedOrigins = {
[baseUrl]: {},
'https://example.com': {},
'http://foobar.com': {},
}
cy.stub(CypressInstance, 'backend')
.callThrough()
.withArgs('get:rendered:html:origins')
.resolves(storedOrigins)
const origins = await mapOrigins(CypressInstance, '*')
expect(origins).to.deep.equal([baseUrl, 'https://example.com', 'http://foobar.com'])
})
it('returns current origin when requesting the current origin', async () => {
cy.stub(CypressInstance, 'backend').callThrough().withArgs('get:rendered:html:origins')
const origins = await mapOrigins(CypressInstance, 'currentOrigin')
expect(origins).to.deep.equal([baseUrl])
expect(CypressInstance.backend).not.to.be.calledWith('get:rendered:html:origins')
})
it('returns specific origin when requesitng a specific origin', async () => {
cy.stub(CypressInstance, 'backend').callThrough().withArgs('get:rendered:html:origins')
const origins = await mapOrigins(CypressInstance, 'https://example.com/random_page?1')
expect(origins).to.deep.equal(['https://example.com'])
expect(CypressInstance.backend).not.to.be.calledWith('get:rendered:html:origins')
})
it('return origins when requesting a list of origins', async () => {
const storedOrigins = {
[baseUrl]: {},
'https://example.com': {},
'http://foobar.com': {},
'https://other.com': {},
}
cy.stub(CypressInstance, 'backend')
.callThrough()
.withArgs('get:rendered:html:origins')
.resolves(storedOrigins)
const origins = await mapOrigins(CypressInstance, ['*', 'https://other.com'])
expect(origins).to.deep.equal([baseUrl, 'https://example.com', 'http://foobar.com', 'https://other.com'])
})
})
describe('getAllHtmlOrigins()', () => {
it('returns rendered html origins from backend', async () => {
const storedOrigins = {
[baseUrl]: {},
'https://example.com': {},
'http://foobar.com': {},
}
cy.stub(CypressInstance, 'backend')
.callThrough()
.withArgs('get:rendered:html:origins')
.resolves(storedOrigins)
const origins = await getAllHtmlOrigins(CypressInstance)
expect(origins).to.deep.equal([baseUrl, 'https://example.com', 'http://foobar.com'])
})
})
})

View File

@@ -0,0 +1,37 @@
import { getStorage, setStorage } from '../../../../src/cy/commands/sessions/storage'
describe('src/cy/commands/sessions/storage', () => {
describe('setStorage()', () => {
it('returns unique origins when requesting all origins', () => {
cy.visit('http://localhost:3500/fixtures/generic.html')
.then(() => {
localStorage.key1 = 'val1'
return setStorage(Cypress, { localStorage: [{ value: { key2: 'val2' } }] })
})
.then(() => {
expect(window.localStorage.key2).equal('val2')
})
.then(() => {
return setStorage(Cypress, {
localStorage: [
// set localStorage on different origin
{ origin: 'http://www.foobar.com:3500', value: { key2: 'val' }, clear: true },
// set localStorage on current origin
{ value: { key3: 'val' }, clear: true },
],
})
})
.then(() => getStorage(Cypress, { origin: ['current_url', 'http://www.foobar.com:3500'] }))
.then((result) => {
expect(result).deep.equal({
localStorage: [
{ origin: 'http://localhost:3500', value: { key3: 'val' } },
{ origin: 'http://www.foobar.com:3500', value: { key2: 'val' } },
],
sessionStorage: [],
})
})
})
})
})

View File

@@ -0,0 +1,321 @@
import { assertLogLength } from '../../support/utils'
describe('src/cy/commands/storage', () => {
let logs: Cypress.Log[]
beforeEach(() => {
logs = []
cy.on('log:added', (attrs, log: Cypress.Log) => {
logs.push(log)
})
})
context('#getAllLocalStorage', () => {
beforeEach(() => {
cy.visit('/fixtures/set-storage-on-multiple-origins.html')
})
it('gets local storage from all origins', () => {
cy.getAllLocalStorage().should('deep.equal', {
'http://localhost:3500': {
key1: 'value1',
key2: 'value2',
},
'http://www.foobar.com:3500': {
key3: 'value3',
key4: 'value4',
},
'http://other.foobar.com:3500': {
key5: 'value5',
key6: 'value6',
},
'http://barbaz.com:3500': {
key7: 'value7',
key8: 'value8',
},
})
})
it('logs once', () => {
cy.getAllLocalStorage().then(() => {
assertLogLength(logs, 2)
expect(logs[0].get('name')).to.eq('visit')
expect(logs[1].get('name')).to.eq('getAllLocalStorage')
})
})
it('does not log when log: false', () => {
cy.getAllLocalStorage({ log: false }).then(() => {
assertLogLength(logs, 1)
expect(logs[0].get('name')).to.eq('visit')
})
})
it('consoleProps includes the storage yielded', () => {
cy.getAllLocalStorage().then(() => {
const consoleProps = logs[1].get('consoleProps')()
expect(consoleProps).to.deep.equal({
Command: 'getAllLocalStorage',
Yielded: {
'http://localhost:3500': {
key1: 'value1',
key2: 'value2',
},
'http://www.foobar.com:3500': {
key3: 'value3',
key4: 'value4',
},
'http://other.foobar.com:3500': {
key5: 'value5',
key6: 'value6',
},
'http://barbaz.com:3500': {
key7: 'value7',
key8: 'value8',
},
},
})
})
})
})
context('#clearAllLocalStorage', () => {
beforeEach(() => {
cy.visit('/fixtures/set-storage-on-multiple-origins.html')
})
it('clears local storage for all origins', () => {
cy.clearAllLocalStorage()
cy.getAllLocalStorage().should('deep.equal', {})
})
it('logs once', () => {
cy.clearAllLocalStorage().then(() => {
assertLogLength(logs, 2)
expect(logs[0].get('name')).to.eq('visit')
expect(logs[1].get('name')).to.eq('clearAllLocalStorage')
})
})
it('does not log when log: false', () => {
cy.clearAllLocalStorage({ log: false }).then(() => {
assertLogLength(logs, 1)
expect(logs[0].get('name')).to.eq('visit')
})
})
})
context('#getAllSessionStorage', () => {
beforeEach(() => {
cy.visit('/fixtures/set-storage-on-multiple-origins.html')
})
it('gets local storage from all origins', () => {
cy.getAllSessionStorage().should('deep.equal', {
'http://localhost:3500': {
key11: 'value11',
key12: 'value12',
},
'http://www.foobar.com:3500': {
key13: 'value13',
key14: 'value14',
},
'http://other.foobar.com:3500': {
key15: 'value15',
key16: 'value16',
},
'http://barbaz.com:3500': {
key17: 'value17',
key18: 'value18',
},
})
})
it('logs once', () => {
cy.getAllSessionStorage().then(() => {
assertLogLength(logs, 2)
expect(logs[0].get('name')).to.eq('visit')
expect(logs[1].get('name')).to.eq('getAllSessionStorage')
})
})
it('does not log when log: false', () => {
cy.getAllSessionStorage({ log: false }).then(() => {
assertLogLength(logs, 1)
expect(logs[0].get('name')).to.eq('visit')
})
})
it('consoleProps includes the storage yielded', () => {
cy.getAllSessionStorage().then(() => {
const consoleProps = logs[1].get('consoleProps')()
expect(consoleProps).to.deep.equal({
Command: 'getAllSessionStorage',
Yielded: {
'http://localhost:3500': {
key11: 'value11',
key12: 'value12',
},
'http://www.foobar.com:3500': {
key13: 'value13',
key14: 'value14',
},
'http://other.foobar.com:3500': {
key15: 'value15',
key16: 'value16',
},
'http://barbaz.com:3500': {
key17: 'value17',
key18: 'value18',
},
},
})
})
})
})
context('#clearAllSessionStorage', () => {
beforeEach(() => {
cy.visit('/fixtures/set-storage-on-multiple-origins.html')
})
it('clears session storage for all origins', () => {
cy.clearAllSessionStorage()
cy.getAllSessionStorage().should('deep.equal', {})
})
it('logs once', () => {
cy.clearAllSessionStorage().then(() => {
assertLogLength(logs, 2)
expect(logs[0].get('name')).to.eq('visit')
expect(logs[1].get('name')).to.eq('clearAllSessionStorage')
})
})
it('does not log when log: false', () => {
cy.clearAllSessionStorage({ log: false }).then(() => {
assertLogLength(logs, 1)
expect(logs[0].get('name')).to.eq('visit')
})
})
})
context('#clearLocalStorage', () => {
it('passes keys onto Cypress.LocalStorage.clear', () => {
const clear = cy.spy(Cypress.LocalStorage, 'clear')
cy.clearLocalStorage('foo').then(() => {
expect(clear).to.be.calledWith('foo')
})
})
it('sets the storages', () => {
const {
localStorage,
} = window
const remoteStorage = cy.state('window').localStorage
const setStorages = cy.spy<InternalCypress.LocalStorage>(Cypress.LocalStorage as InternalCypress.LocalStorage, 'setStorages')
cy.clearLocalStorage().then(() => {
expect(setStorages).to.be.calledWith(localStorage, remoteStorage)
})
})
it('unsets the storages', () => {
const unsetStorages = cy.spy<InternalCypress.LocalStorage>(Cypress.LocalStorage as InternalCypress.LocalStorage, 'unsetStorages')
cy.clearLocalStorage().then(() => {
expect(unsetStorages).to.be.called
})
})
it('sets subject to remote localStorage', () => {
const ls = cy.state('window').localStorage
cy.clearLocalStorage().then((remote) => {
expect(remote).to.eq(ls)
})
})
describe('test:before:run', () => {
it('clears localStorage before each test run', () => {
const clear = cy.spy(Cypress.LocalStorage, 'clear')
Cypress.emit('test:before:run', {})
expect(clear).not.to.be.called
})
})
describe('errors', () => {
it('throws when being passed a non string or regexp', (done) => {
cy.on('fail', (err) => {
expect(err.message).to.include('`cy.clearLocalStorage()` must be called with either a string or regular expression.')
expect(err.docsUrl).to.include('https://on.cypress.io/clearlocalstorage')
done()
})
// @ts-expect-error
cy.clearLocalStorage(1)
})
})
describe('.log', () => {
beforeEach(function () {
cy.on('log:added', (attrs, log) => {
this.lastLog = log
})
return null
})
it('ends immediately', () => {
cy.clearLocalStorage().then(function () {
const { lastLog } = this
expect(lastLog.get('ended')).to.be.true
expect(lastLog.get('state')).to.eq('passed')
})
})
it('snapshots immediately', () => {
cy.clearLocalStorage().then(function () {
const { lastLog } = this
expect(lastLog.get('snapshots').length).to.eq(1)
expect(lastLog.get('snapshots')[0]).to.be.an('object')
})
})
})
describe('without log', () => {
beforeEach(function () {
cy.on('log:added', (attrs, log) => {
this.lastLog = log
})
return null
})
it('log is disabled', () => {
cy.clearLocalStorage('foo', { log: false }).then(function () {
const { lastLog } = this
expect(lastLog).to.be.undefined
})
})
it('log is disabled without key', () => {
cy.clearLocalStorage({ log: false }).then(function () {
const { lastLog } = this
expect(lastLog).to.be.undefined
})
})
})
})
})

View File

@@ -1,52 +0,0 @@
import { findCrossOriginLogs } from '../../../../support/utils'
context('cy.origin local storage', { browser: '!webkit' }, () => {
beforeEach(() => {
cy.visit('/fixtures/primary-origin.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
})
it('.clearLocalStorage()', () => {
cy.origin('http://www.foobar.com:3500', () => {
cy.window().then((win) => {
win.localStorage.setItem('foo', 'bar')
expect(win.localStorage.getItem('foo')).to.equal('bar')
})
cy.clearLocalStorage().should((localStorage) => {
expect(localStorage.length).to.equal(0)
expect(localStorage.getItem('foo')).to.be.null
})
})
})
context('#consoleProps', () => {
let logs: Map<string, any>
beforeEach(() => {
logs = new Map()
cy.on('log:changed', (attrs, log) => {
logs.set(attrs.id, log)
})
})
it('.clearLocalStorage()', () => {
cy.origin('http://www.foobar.com:3500', () => {
cy.window().then((win) => {
win.localStorage.setItem('foo', 'bar')
expect(win.localStorage.getItem('foo')).to.equal('bar')
})
cy.clearLocalStorage()
})
cy.shouldWithTimeout(() => {
const { consoleProps } = findCrossOriginLogs('clearLocalStorage', logs, 'foobar.com')
expect(consoleProps.Command).to.equal('clearLocalStorage')
expect(consoleProps.Yielded).to.be.null
})
})
})
})

View File

@@ -219,7 +219,7 @@ it('verifies number of cy commands', () => {
'focused', 'get', 'contains', 'shadow', 'within', 'request', 'session', 'screenshot', 'task', 'find', 'filter', 'not',
'children', 'eq', 'closest', 'first', 'last', 'next', 'nextAll', 'nextUntil', 'parent', 'parents', 'parentsUntil', 'prev',
'prevAll', 'prevUntil', 'siblings', 'wait', 'title', 'window', 'document', 'viewport', 'server', 'route', 'intercept', 'origin',
'mount', 'as', 'root',
'mount', 'as', 'root', 'getAllLocalStorage', 'clearAllLocalStorage', 'getAllSessionStorage', 'clearAllSessionStorage',
]
const addedCommands = Cypress._.difference(actualCommands, expectedCommands)
const removedCommands = Cypress._.difference(expectedCommands, actualCommands)

View File

@@ -0,0 +1,120 @@
import { findCrossOriginLogs } from '../../../../support/utils'
context('cy.origin storage', { browser: '!webkit' }, () => {
beforeEach(() => {
cy.visit('/fixtures/primary-origin.html')
cy.get('a[data-cy="cross-origin-secondary-link"]').click()
})
it('.getAllLocalStorage', () => {
cy.origin('http://www.foobar.com:3500', () => {
cy.visit('/fixtures/set-storage-on-multiple-origins.html')
cy.getAllLocalStorage().should('deep.equal', {
'http://localhost:3500': {
key1: 'value1',
key2: 'value2',
},
'http://www.foobar.com:3500': {
key3: 'value3',
key4: 'value4',
},
'http://other.foobar.com:3500': {
key5: 'value5',
key6: 'value6',
},
'http://barbaz.com:3500': {
key7: 'value7',
key8: 'value8',
},
})
})
})
it('.clearAllLocalStorage', () => {
cy.origin('http://www.foobar.com:3500', () => {
cy.visit('/fixtures/set-storage-on-multiple-origins.html')
cy.clearAllLocalStorage()
cy.getAllLocalStorage().should('deep.equal', {})
})
})
it('.getAllSessionStorage', () => {
cy.origin('http://www.foobar.com:3500', () => {
cy.visit('/fixtures/set-storage-on-multiple-origins.html')
cy.getAllSessionStorage().should('deep.equal', {
'http://localhost:3500': {
key11: 'value11',
key12: 'value12',
},
'http://www.foobar.com:3500': {
key13: 'value13',
key14: 'value14',
},
'http://other.foobar.com:3500': {
key15: 'value15',
key16: 'value16',
},
'http://barbaz.com:3500': {
key17: 'value17',
key18: 'value18',
},
})
})
})
it('.clearAllSessionStorage', () => {
cy.origin('http://www.foobar.com:3500', () => {
cy.visit('/fixtures/set-storage-on-multiple-origins.html')
cy.clearAllSessionStorage()
cy.getAllSessionStorage().should('deep.equal', {})
})
})
it('.clearLocalStorage()', () => {
cy.origin('http://www.foobar.com:3500', () => {
cy.window().then((win) => {
win.localStorage.setItem('foo', 'bar')
expect(win.localStorage.getItem('foo')).to.equal('bar')
})
cy.clearLocalStorage().should((localStorage) => {
expect(localStorage.length).to.equal(0)
expect(localStorage.getItem('foo')).to.be.null
})
})
})
context('#consoleProps', () => {
let logs: Map<string, any>
beforeEach(() => {
logs = new Map()
cy.on('log:changed', (attrs, log) => {
logs.set(attrs.id, log)
})
})
it('.clearLocalStorage()', () => {
cy.origin('http://www.foobar.com:3500', () => {
cy.window().then((win) => {
win.localStorage.setItem('foo', 'bar')
expect(win.localStorage.getItem('foo')).to.equal('bar')
})
cy.clearLocalStorage()
})
cy.shouldWithTimeout(() => {
const { consoleProps } = findCrossOriginLogs('clearLocalStorage', logs, 'foobar.com')
expect(consoleProps.Command).to.equal('clearLocalStorage')
expect(consoleProps.Yielded).to.be.null
})
})
})
})

View File

@@ -206,7 +206,7 @@ describe('cy.origin Cypress API', { browser: '!webkit' }, () => {
context('not supported', () => {
it('throws an error when a user attempts to call Cypress.session.clearAllSavedSessions() inside of cy.origin', (done) => {
cy.on('fail', (err) => {
expect(err.message).to.equal('`Cypress.session.*` methods are not supported in the `cy.switchToDomain()` callback. Consider using them outside of the callback instead.')
expect(err.message).to.equal('`Cypress.session.*` methods are not supported in the `cy.origin()` callback. Consider using them outside of the callback instead.')
expect(err.docsUrl).to.equal('https://on.cypress.io/session-api')
done()
})

View File

@@ -0,0 +1,9 @@
<h1>Set storage on multiple origins</h1>
<script>
const $ = Cypress.$
$(`<iframe src="http://localhost:3500/fixtures/set-storage.html?nums=1,2"></iframe>`).appendTo($('body'))
$(`<iframe src="http://www.foobar.com:3500/fixtures/set-storage.html?nums=3,4"></iframe>`).appendTo($('body'))
$(`<iframe src="http://other.foobar.com:3500/fixtures/set-storage.html?nums=5,6"></iframe>`).appendTo($('body'))
$(`<iframe src="http://barbaz.com:3500/fixtures/set-storage.html?nums=7,8"></iframe>`).appendTo($('body'))
</script>

View File

@@ -0,0 +1,10 @@
<h1>Set storage</h1>
<script>
const urlParams = new URLSearchParams(window.location.search)
const nums = urlParams.get('nums').split(',').map(Number)
nums.forEach((num) => {
window.localStorage.setItem(`key${num}`, `value${num}`)
window.sessionStorage.setItem(`key${num + 10}`, `value${num + 10}`)
})
</script>

View File

@@ -22,7 +22,7 @@ import * as Files from './files'
import * as Fixtures from './fixtures'
import LocalStorage from './local_storage'
import Storage from './storage'
import * as Location from './location'
@@ -65,7 +65,7 @@ export const allCommands = {
Exec,
Files,
Fixtures,
LocalStorage,
Storage,
Location,
Misc,
Origin,

View File

@@ -1,51 +0,0 @@
import _ from 'lodash'
import $errUtils from '../../cypress/error_utils'
import $LocalStorage from '../../cypress/local_storage'
const clearLocalStorage = (state, keys) => {
const local = window.localStorage
const remote = state('window').localStorage
// set our localStorage and the remote localStorage
$LocalStorage.setStorages(local, remote)
// clear the keys
$LocalStorage.clear(keys)
// and then unset the references
$LocalStorage.unsetStorages()
// return the remote localStorage object
return remote
}
export default (Commands, Cypress, cy, state) => {
Commands.addAll({
clearLocalStorage (keys, options: Partial<Cypress.Loggable> = {}) {
if (_.isPlainObject(keys)) {
options = keys
keys = null
}
_.defaults(options, { log: true })
// bail if we have keys and we're not a string and we're not a regexp
if (keys && !_.isString(keys) && !_.isRegExp(keys)) {
$errUtils.throwErrByPath('clearLocalStorage.invalid_argument')
}
const remote = clearLocalStorage(state, keys)
if (options.log) {
Cypress.log({
snapshot: true,
end: true,
})
}
// return the remote local storage object
return remote
},
})
}

View File

@@ -1,11 +1,8 @@
import _ from 'lodash'
import { $Location } from '../../../cypress/location'
import type { ServerSessionData } from '@packages/types'
import {
getCurrentOriginStorage,
setPostMessageLocalStorage,
getPostMessageLocalStorage,
} from './utils'
import _ from 'lodash'
import { getAllHtmlOrigins } from './origins'
import { clearStorage, getStorage, setStorage } from './storage'
type ActiveSessions = Cypress.Commands.Session.ActiveSessions
type SessionData = Cypress.Commands.Session.SessionData
@@ -63,66 +60,6 @@ export default class SessionsManager {
this.cy.state('activeSessions', clearedSessions)
}
mapOrigins = async (origins: string | Array<string>): Promise<Array<string>> => {
const getOrigins = this.Cypress.Promise.map(
([] as string[]).concat(origins), async (v) => {
if (v === '*') {
return await this.getAllHtmlOrigins()
}
if (v === 'currentOrigin') {
return $Location.create(window.location.href).origin
}
return $Location.create(v).origin
},
)
return _.uniq(_.flatten(await getOrigins))
}
_setStorageOnOrigins = async (originOptions) => {
const specWindow = this.cy.state('specWindow')
const currentOrigin = $Location.create(window.location.href).origin
const currentOriginIndex = _.findIndex(originOptions, { origin: currentOrigin })
if (currentOriginIndex !== -1) {
const opts = originOptions.splice(currentOriginIndex, 1)[0]
if (!_.isEmpty(opts.localStorage)) {
if (opts.localStorage.clear) {
window.localStorage.clear()
}
_.each(opts.localStorage.value, (val, key) => localStorage.setItem(key, val))
}
if (opts.sessionStorage) {
if (opts.sessionStorage.clear) {
window.sessionStorage.clear()
}
_.each(opts.sessionStorage.value, (val, key) => sessionStorage.setItem(key, val))
}
}
if (_.isEmpty(originOptions)) {
return
}
await setPostMessageLocalStorage(specWindow, originOptions)
}
getAllHtmlOrigins = async () => {
const currentOrigin = $Location.create(window.location.href).origin
const storedOrigins = await this.Cypress.backend('get:rendered:html:origins')
const origins = [..._.keys(storedOrigins), currentOrigin]
return _.uniq(origins)
}
// this the public api exposed to consumers as Cypress.session
sessions = {
defineSession: (options = {} as any): SessionData => {
@@ -156,7 +93,7 @@ export default class SessionsManager {
window.sessionStorage.clear()
await Promise.all([
this.sessions.clearStorage(),
clearStorage(this.Cypress),
this.sessions.clearCookies(),
])
},
@@ -170,7 +107,7 @@ export default class SessionsManager {
},
setSessionData: async (data) => {
const allHtmlOrigins = await this.getAllHtmlOrigins()
const allHtmlOrigins = await getAllHtmlOrigins(this.Cypress)
let _localStorage = data.localStorage || []
let _sessionStorage = data.sessionStorage || []
@@ -186,7 +123,7 @@ export default class SessionsManager {
})
await Promise.all([
this.sessions.setStorage({ localStorage: _localStorage, sessionStorage: _sessionStorage }),
setStorage(this.Cypress, { localStorage: _localStorage, sessionStorage: _sessionStorage }),
this.sessions.setCookies(data.cookies),
])
},
@@ -205,7 +142,7 @@ export default class SessionsManager {
getCurrentSessionData: async () => {
const [storage, cookies] = await Promise.all([
this.sessions.getStorage({ origin: '*' }),
getStorage(this.Cypress, { origin: '*' }),
this.sessions.getCookies(),
])
@@ -218,110 +155,5 @@ export default class SessionsManager {
getSession: (id: string): Promise<ServerSessionData> => {
return this.Cypress.backend('get:session', id)
},
/**
* 1) if we only need currentOrigin localStorage, access sync
* 2) if cross-origin http, we need to load in iframe from our proxy that will intercept all http reqs at /__cypress/automation/*
* and postMessage() the localStorage value to us
* 3) if cross-origin https, since we pass-thru https connections in the proxy, we need to
* send a message telling our proxy server to intercept the next req to the https domain,
* then follow 2)
*/
getStorage: async (options = {}) => {
const specWindow = this.cy.state('specWindow')
if (!_.isObject(options)) {
throw new Error('getStorage() takes an object')
}
const opts = _.defaults({}, options, {
origin: 'currentOrigin',
})
const currentOrigin = $Location.create(window.location.href).origin
const origins: Array<string> = await this.mapOrigins(opts.origin)
const results = {
localStorage: [] as any[],
sessionStorage: [] as any[],
}
function pushValue (origin, value) {
if (!_.isEmpty(value.localStorage)) {
results.localStorage.push({ origin, value: value.localStorage })
}
if (!_.isEmpty(value.sessionStorage)) {
results.sessionStorage.push({ origin, value: value.sessionStorage })
}
}
const currentOriginIndex = origins.indexOf(currentOrigin)
if (currentOriginIndex !== -1) {
origins.splice(currentOriginIndex, 1)
const currentOriginStorage = getCurrentOriginStorage()
pushValue(currentOrigin, currentOriginStorage)
}
if (_.isEmpty(origins)) {
return results
}
if (currentOrigin.startsWith('https:')) {
_.remove(origins, (v) => v.startsWith('http:'))
}
const postMessageResults = await getPostMessageLocalStorage(specWindow, origins)
postMessageResults.forEach((val) => {
pushValue(val[0], val[1])
})
return results
},
clearStorage: async () => {
const origins = await this.getAllHtmlOrigins()
const originOptions = origins.map((v) => ({ origin: v, clear: true }))
await this.sessions.setStorage({
localStorage: originOptions,
sessionStorage: originOptions,
})
},
setStorage: async (options: any, clearAll = false) => {
const currentOrigin = $Location.create(window.location.href).origin as string
const mapToCurrentOrigin = (v) => ({ ...v, origin: (v.origin && v.origin !== 'currentOrigin') ? $Location.create(v.origin).origin : currentOrigin })
const mappedLocalStorage = _.map(options.localStorage, (v) => {
const mapped = { origin: v.origin, localStorage: _.pick(v, 'value', 'clear') }
if (clearAll) {
mapped.localStorage.clear = true
}
return mapped
}).map(mapToCurrentOrigin)
const mappedSessionStorage = _.map(options.sessionStorage, (v) => {
const mapped = { origin: v.origin, sessionStorage: _.pick(v, 'value', 'clear') }
if (clearAll) {
mapped.sessionStorage.clear = true
}
return mapped
}).map(mapToCurrentOrigin)
const storageOptions = _.map(_.groupBy(mappedLocalStorage.concat(mappedSessionStorage), 'origin'), (v) => _.merge({}, ...v))
await this._setStorageOnOrigins(storageOptions)
},
}
}

View File

@@ -0,0 +1,28 @@
import Bluebird from 'bluebird'
import { $Location } from '../../../cypress/location'
export async function mapOrigins (Cypress: Cypress.Cypress, origins: string | string[]): Promise<string[]> {
const getOrigins = Bluebird.map(
([] as string[]).concat(origins), async (origin) => {
if (origin === '*') {
return await getAllHtmlOrigins(Cypress)
}
if (origin === 'currentOrigin') {
return window.location.origin
}
return $Location.create(origin).origin
},
)
return _.uniq(_.flatten(await getOrigins))
}
export async function getAllHtmlOrigins (Cypress: Cypress.Cypress) {
const currentOrigin = window.location.origin
const storedOrigins = await Cypress.backend('get:rendered:html:origins')
const origins = [..._.keys(storedOrigins), currentOrigin]
return _.uniq(origins)
}

View File

@@ -0,0 +1,158 @@
import _ from 'lodash'
import { $Location } from '../../../cypress/location'
import { getAllHtmlOrigins, mapOrigins } from './origins'
import { getCurrentOriginStorage, getPostMessageLocalStorage, setPostMessageLocalStorage } from './utils'
export type StorageType = 'localStorage' | 'sessionStorage'
interface GetStorageOptions {
origin?: '*' | 'currentOrigin' | string | string[]
}
interface OriginStorageOptions {
clear?: boolean
origin?: string | string[]
value?: any
}
interface SetStoragesOptions {
localStorage?: OriginStorageOptions[]
sessionStorage?: OriginStorageOptions[]
}
/**
* 1) if we only need currentOrigin localStorage, access sync
* 2) if cross-origin http, we need to load in iframe from our proxy that will intercept all http reqs at /__cypress/automation/*
* and postMessage() the localStorage value to us
* 3) if cross-origin https, since we pass-thru https connections in the proxy, we need to
* send a message telling our proxy server to intercept the next req to the https domain,
* then follow 2)
*/
export async function getStorage (Cypress: Cypress.Cypress, options: GetStorageOptions = {}): Promise<Cypress.Storages> {
const specWindow = Cypress.state('specWindow')
if (!_.isObject(options)) {
throw new Error('getStorage() takes an object')
}
const opts = _.defaults({}, options, {
origin: 'currentOrigin',
})
const currentOrigin = window.location.origin
const origins: Array<string> = await mapOrigins(Cypress, opts.origin)
const results = {
localStorage: [] as Cypress.OriginStorage[],
sessionStorage: [] as Cypress.OriginStorage[],
}
function pushValue (origin, value) {
if (!_.isEmpty(value.localStorage)) {
results.localStorage.push({ origin, value: value.localStorage })
}
if (!_.isEmpty(value.sessionStorage)) {
results.sessionStorage.push({ origin, value: value.sessionStorage })
}
}
const currentOriginIndex = origins.indexOf(currentOrigin)
if (currentOriginIndex !== -1) {
origins.splice(currentOriginIndex, 1)
const currentOriginStorage = getCurrentOriginStorage()
pushValue(currentOrigin, currentOriginStorage)
}
if (_.isEmpty(origins)) {
return results
}
if (currentOrigin.startsWith('https:')) {
_.remove(origins, (v) => v.startsWith('http:'))
}
const postMessageResults = await getPostMessageLocalStorage(specWindow, origins)
postMessageResults.forEach((val) => {
pushValue(val[0], val[1])
})
return results
}
export async function clearStorage (Cypress: Cypress.Cypress, type?: StorageType) {
const origins = await getAllHtmlOrigins(Cypress)
const originOptions = origins.map((origin) => ({ origin, clear: true }))
const options: SetStoragesOptions = {}
if (!type || type === 'localStorage') {
options.localStorage = originOptions
}
if (!type || type === 'sessionStorage') {
options.sessionStorage = originOptions
}
await setStorage(Cypress, options)
}
async function setStorageOnOrigins (Cypress: Cypress.Cypress, originOptions) {
const specWindow = Cypress.state('specWindow')
const currentOrigin = window.location.origin
const currentOriginIndex = _.findIndex(originOptions, { origin: currentOrigin })
if (currentOriginIndex !== -1) {
const opts = originOptions.splice(currentOriginIndex, 1)[0]
if (!_.isEmpty(opts.localStorage)) {
if (opts.localStorage.clear) {
window.localStorage.clear()
}
_.each(opts.localStorage.value, (val, key) => localStorage.setItem(key, val))
}
if (opts.sessionStorage) {
if (opts.sessionStorage.clear) {
window.sessionStorage.clear()
}
_.each(opts.sessionStorage.value, (val, key) => sessionStorage.setItem(key, val))
}
}
if (_.isEmpty(originOptions)) {
return
}
await setPostMessageLocalStorage(specWindow, originOptions)
}
export async function setStorage (Cypress: Cypress.Cypress, options: SetStoragesOptions) {
const currentOrigin = window.location.origin
function mapToCurrentOrigin (v) {
return {
...v,
origin: (v.origin && v.origin !== 'currentOrigin')
? $Location.create(v.origin).origin
: currentOrigin,
}
}
const mappedLocalStorage = _.map(options.localStorage, (v) => {
return mapToCurrentOrigin({ origin: v.origin, localStorage: _.pick(v, 'value', 'clear') })
})
const mappedSessionStorage = _.map(options.sessionStorage, (v) => {
return mapToCurrentOrigin({ origin: v.origin, sessionStorage: _.pick(v, 'value', 'clear') })
})
const storageOptions = _.map(_.groupBy(mappedLocalStorage.concat(mappedSessionStorage), 'origin'), (v) => _.merge({}, ...v))
await setStorageOnOrigins(Cypress, storageOptions)
}

View File

@@ -0,0 +1,108 @@
import _ from 'lodash'
import $errUtils from '../../cypress/error_utils'
import $LocalStorage from '../../cypress/local_storage'
import { clearStorage, getStorage, StorageType } from './sessions/storage'
type Options = Partial<Cypress.Loggable>
const clearLocalStorage = (state, keys) => {
const local = window.localStorage
const remote = state('window').localStorage
// set our localStorage and the remote localStorage
$LocalStorage.setStorages(local, remote)
// clear the keys
$LocalStorage.clear(keys)
// and then unset the references
$LocalStorage.unsetStorages()
// return the remote localStorage object
return remote
}
const getAllStorage = async (type: StorageType, Cypress: InternalCypress.Cypress, userOptions: Options = {}) => {
const options: Options = {
log: true,
...userOptions,
}
let storageByOrigin: Cypress.StorageByOrigin = {}
if (options.log) {
Cypress.log({
consoleProps () {
const obj = {}
if (Object.keys(storageByOrigin).length) {
obj['Yielded'] = storageByOrigin
}
return obj
},
})
}
const storages = await getStorage(Cypress, { origin: '*' })
storageByOrigin = storages[type].reduce((memo, storage) => {
memo[storage.origin] = storage.value
return memo
}, {} as Cypress.StorageByOrigin)
return storageByOrigin
}
const clearAllStorage = async (type: StorageType, Cypress: InternalCypress.Cypress, userOptions: Options = {}) => {
const options: Options = {
log: true,
...userOptions,
}
if (options.log) {
Cypress.log({})
}
await clearStorage(Cypress, type)
return null
}
export default (Commands, Cypress: InternalCypress.Cypress, cy, state, config) => {
Commands.addAll({
getAllLocalStorage: getAllStorage.bind(null, 'localStorage', Cypress),
getAllSessionStorage: getAllStorage.bind(null, 'sessionStorage', Cypress),
clearAllLocalStorage: clearAllStorage.bind(null, 'localStorage', Cypress),
clearAllSessionStorage: clearAllStorage.bind(null, 'sessionStorage', Cypress),
clearLocalStorage (keys, options: Options = {}) {
if (_.isPlainObject(keys)) {
options = keys
keys = null
}
_.defaults(options, { log: true })
// bail if we have keys and we're not a string and we're not a regexp
if (keys && !_.isString(keys) && !_.isRegExp(keys)) {
$errUtils.throwErrByPath('clearLocalStorage.invalid_argument')
}
const remote = clearLocalStorage(state, keys)
if (options.log) {
Cypress.log({
snapshot: true,
end: true,
})
}
// return the remote local storage object
return remote
},
})
}

View File

@@ -1277,7 +1277,7 @@ export default {
docsUrl: 'https://on.cypress.io/github-issue/20721',
},
Cypress_session: {
message: `\`Cypress.session.*\` methods are not supported in the ${cmd('switchToDomain')} callback. Consider using them outside of the callback instead.`,
message: `\`Cypress.session.*\` methods are not supported in the ${cmd('origin')} callback. Consider using them outside of the callback instead.`,
docsUrl: 'https://on.cypress.io/session-api',
},
},

View File

@@ -63,6 +63,16 @@ declare namespace Cypress {
interface Backend {
(task: 'cross:origin:cookies:received'): Promise<void>
(task: 'get:rendered:html:origins'): Promise<string[]>
}
}
declare namespace InternalCypress {
interface Cypress extends Cypress.Cypress, NodeEventEmitter {}
interface LocalStorage extends Cypress.LocalStorage {
setStorages: (local, remote) => LocalStorage
unsetStorages: () => LocalStorage
}
}

View File

@@ -218,7 +218,7 @@
"content": "Record a run to see your test results in Cypress Cloud. You can then optimize your test suite, debug failing and flaky tests, and integrate with your favorite tools."
}
},
"runAllSpecs": "Run {n} spec | Run {n} specs"
"runSelectedSpecs": "Run {n} spec | Run {n} specs"
},
"noResults": {
"defaultMessage": "No results matched your search:",

View File

@@ -0,0 +1 @@
export const isRunMode = window.__CYPRESS_MODE__ === 'run' && window.top === window

View File

@@ -27,7 +27,7 @@ export const PACKAGE_MANAGERS = ['npm', 'yarn', 'pnpm'] as const
// for a new major version of Cypress
export const MAJOR_VERSION_FOR_CONTENT = '11'
export const RUN_ALL_SPECS_KEY = '__all'
export const RUN_ALL_SPECS_KEY = '__all' as const
export const RUN_ALL_SPECS: SpecFile = {
name: 'All E2E Specs',

View File

@@ -1,6 +0,0 @@
**/dist
**/*.d.ts
**/package-lock.json
**/tsconfig.json
**/cypress/fixtures
**/__snapshots__

View File

@@ -261,8 +261,6 @@ export function snapshotRequire (
return path.resolve(projectBaseDir, p)
} catch (err) {
logError(err)
// eslint-disable-next-line no-debugger
debugger
}
return

View File

@@ -1,107 +0,0 @@
exports['e2e domain / passes'] = `
====================================================================================================
(Run Starting)
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Cypress: 1.2.3 │
│ Browser: FooBrowser 88 │
│ Specs: 2 found (domain.cy.js, domain_2.cy.js) │
│ Searched: cypress/e2e/domain* │
└────────────────────────────────────────────────────────────────────────────────────────────────┘
────────────────────────────────────────────────────────────────────────────────────────────────────
Running: domain.cy.js (1 of 2)
localhost
✓ can visit
com.au
✓ can visit
herokuapp.com
✓ can visit
3 passing
(Results)
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Tests: 3 │
│ Passing: 3 │
│ Failing: 0 │
│ Pending: 0 │
│ Skipped: 0 │
│ Screenshots: 0 │
│ Video: true │
│ Duration: X seconds │
│ Spec Ran: domain.cy.js │
└────────────────────────────────────────────────────────────────────────────────────────────────┘
(Video)
- Started processing: Compressing to 32 CRF
- Finished processing: /XXX/XXX/XXX/cypress/videos/domain.cy.js.mp4 (X second)
────────────────────────────────────────────────────────────────────────────────────────────────────
Running: domain_2.cy.js (2 of 2)
localhost
✓ can visit
com.au
✓ can visit
herokuapp.com
✓ can visit
3 passing
(Results)
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Tests: 3 │
│ Passing: 3 │
│ Failing: 0 │
│ Pending: 0 │
│ Skipped: 0 │
│ Screenshots: 0 │
│ Video: true │
│ Duration: X seconds │
│ Spec Ran: domain_2.cy.js │
└────────────────────────────────────────────────────────────────────────────────────────────────┘
(Video)
- Started processing: Compressing to 32 CRF
- Finished processing: /XXX/XXX/XXX/cypress/videos/domain_2.cy.js.mp4 (X second)
====================================================================================================
(Run Finished)
Spec Tests Passing Failing Pending Skipped
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ ✔ domain.cy.js XX:XX 3 3 - - - │
├────────────────────────────────────────────────────────────────────────────────────────────────┤
│ ✔ domain_2.cy.js XX:XX 3 3 - - - │
└────────────────────────────────────────────────────────────────────────────────────────────────┘
✔ All specs passed! XX:XX 6 6 - - -
`

View File

@@ -17,13 +17,6 @@ exports['e2e sessions / session tests'] = `
Running: session.cy.js (1 of 1)
cross origin automations
✓ get storage
✓ get storage w/ sessionStorage
✓ set storage
✓ get localStorage from all origins
✓ only gets localStorage from origins visited in test
with a blank session
✓ t1
✓ t2
@@ -99,15 +92,15 @@ exports['e2e sessions / session tests'] = `
✓ clears only secure context data - 2/2
40 passing
35 passing
1 pending
(Results)
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ Tests: 41
│ Passing: 40
│ Tests: 36
│ Passing: 35
│ Failing: 0 │
│ Pending: 1 │
│ Skipped: 0 │
@@ -125,9 +118,9 @@ exports['e2e sessions / session tests'] = `
Spec Tests Passing Failing Pending Skipped
┌────────────────────────────────────────────────────────────────────────────────────────────────┐
│ ✔ session.cy.js XX:XX 41 40 - 1 - │
│ ✔ session.cy.js XX:XX 36 35 - 1 - │
└────────────────────────────────────────────────────────────────────────────────────────────────┘
✔ All specs passed! XX:XX 41 40 - 1 -
✔ All specs passed! XX:XX 36 35 - 1 -
`

View File

@@ -90,7 +90,6 @@ Error: Webpack Compilation Error
./cypress/e2e/typescript_syntax_error.cy.tsXX:XX
Module parse failed: Unexpected token (3:19)
File was processed with these loaders:
* relative/path/to/webpack-preprocessor/dist/lib/cross-origin-callback-loader.js
* relative/path/to/webpack-batteries-included-preprocessor/node_modules/ts-loader/index.js
You may need an additional loader to handle the result of these loaders.
| // The code below is ignored by eslint

View File

@@ -1,19 +0,0 @@
/* eslint-disable no-undef */
describe('localhost', () => {
it('can visit', () => {
cy.visit('http://app.localhost:4848')
})
})
describe('com.au', () => {
it('can visit', () => {
cy.visit('http://foo.bar.baz.com.au:4848')
})
})
describe('herokuapp.com', () => {
it('can visit', () => {
cy.visit('https://cypress-example.herokuapp.com')
cy.contains('Getting Started with Node on Heroku')
})
})

View File

@@ -1,19 +0,0 @@
/* eslint-disable no-undef */
describe('localhost', () => {
it('can visit', () => {
cy.visit('http://app.localhost:4848')
})
})
describe('com.au', () => {
it('can visit', () => {
cy.visit('http://foo.bar.baz.com.au:4848')
})
})
describe('herokuapp.com', () => {
it('can visit', () => {
cy.visit('https://cypress-example.herokuapp.com')
cy.contains('Getting Started with Node on Heroku')
})
})

View File

@@ -4,5 +4,6 @@ module.exports = defineConfig({
e2e: {
experimentalRunAllSpecs: true,
supportFile: false,
specPattern: '**/*.cy.js',
},
})

View File

@@ -0,0 +1,3 @@
it('runs folder-c/spec-a', () => {
expect(true).eq(true)
})

View File

@@ -0,0 +1,3 @@
it('runs folder-c/spec-b', () => {
expect(true).eq(true)
})

View File

@@ -38,102 +38,6 @@ const sessionUser = (name = 'user0', cacheAcrossSpecs = false) => {
})
}
describe('cross origin automations', function () {
it('get storage', () => {
cy.visit('https://localhost:4466/cross_origin_iframe/foo')
.then(() => {
localStorage.key1 = 'val1'
})
.then(() => Cypress.session.getStorage({ origin: ['https://127.0.0.1:44665', 'current_origin'] }))
.then((result) => {
expect(result).deep.eq({
localStorage: [
{ origin: 'https://localhost:4466', value: { key1: 'val1' } },
{ origin: 'https://127.0.0.1:44665', value: { name: 'foo' } },
],
sessionStorage: [],
})
})
})
it('get storage w/ sessionStorage', () => {
cy.visit('https://localhost:4466/cross_origin_iframe/foo')
.then(() => {
localStorage.key1 = 'val'
sessionStorage.key1 = 'val'
})
.then(() => Cypress.session.getStorage({ origin: ['https://127.0.0.1:44665', 'current_origin'] }))
.then((result) => {
expect(result).deep.eq({
localStorage: [
{ origin: 'https://localhost:4466', value: { key1: 'val' } },
{ origin: 'https://127.0.0.1:44665', value: { name: 'foo' } },
],
sessionStorage: [
{ origin: 'https://localhost:4466', value: { key1: 'val' } },
],
})
})
})
it('set storage', () => {
cy.visit('https://localhost:4466/cross_origin_iframe/foo')
.then(() => {
localStorage.key1 = 'val1'
})
.then(() => Cypress.session.setStorage({ localStorage: [{ value: { key2: 'val2' } }] }))
.then(() => {
expect(window.localStorage.key2).eq('val2')
})
.then(() => {
return Cypress.session.setStorage({
localStorage: [
// set localStorage on different origin
{ origin: 'https://127.0.0.1:44665', value: { key2: 'val' }, clear: true },
// set localStorage on current origin
{ value: { key3: 'val' }, clear: true },
],
})
})
.then(() => Cypress.session.getStorage({ origin: ['current_url', 'https://127.0.0.1:44665'] }))
.then((result) => {
expect(result).deep.eq({
localStorage: [
{ origin: 'https://localhost:4466', value: { key3: 'val' } },
{ origin: 'https://127.0.0.1:44665', value: { key2: 'val' } },
],
sessionStorage: [],
})
})
})
it('get localStorage from all origins', () => {
cy.visit('https://localhost:4466/cross_origin_iframe/foo')
.then(() => {
localStorage.key1 = 'val1'
})
.then(() => Cypress.session.getStorage({ origin: '*' }))
.then((result) => {
expect(result.localStorage).deep.eq([{ origin: 'https://localhost:4466', value: { key1: 'val1' } }, { origin: 'https://127.0.0.1:44665', value: { name: 'foo' } }])
})
})
it('only gets localStorage from origins visited in test', () => {
cy.visit('https://localhost:4466/form')
.then(() => {
localStorage.key1 = 'val1'
})
.then(() => Cypress.session.getStorage({ origin: '*' }))
.then((result) => {
expect(result.localStorage).deep.eq([{ origin: 'https://localhost:4466', value: { key1: 'val1' } }])
})
})
})
describe('with a blank session', () => {
beforeEach(() => {
cy.session('sess1',

View File

@@ -1,24 +0,0 @@
const systemTests = require('../lib/system-tests').default
const hosts = {
'app.localhost': '127.0.0.1',
'foo.bar.baz.com.au': '127.0.0.1',
}
describe('e2e domain', () => {
systemTests.setup({
servers: {
port: 4848,
static: true,
},
})
systemTests.it('passes', {
spec: 'domain*',
snapshot: true,
video: false,
config: {
hosts,
},
})
})